Back to list
oakoss

tanstack-query

by oakoss

Open-source SaaS starter kit with React, TanStack, and Better Auth

0🍴 0📅 Jan 26, 2026

SKILL.md


name: tanstack-query description: Use TanStack Query for data fetching and caching. Use when implementing queries, mutations, infinite queries, or cache invalidation.

TanStack Query

Mental Model

TanStack Query is an async state manager, NOT a data fetching library. It doesn't fetch data - you provide a queryFn that returns a Promise. React Query handles caching, deduplication, background updates, and stale data management.

Key distinctions:

ConceptClient StateServer State (React Query)
OwnershipYou control completelyPersisted remotely
AvailabilitySynchronousAsynchronous
UpdatesPredictableCan become outdated
ManagementuseState/ZustandTanStack Query

Query keys = dependency array: Parameters used in your queryFn must appear in the queryKey. This ensures automatic refetches when dependencies change and prevents stale closure bugs.

Basic Query

import { useQuery } from '@tanstack/react-query';

const { data, error, isPending, isFetching } = useQuery({
  queryKey: ['posts'],
  queryFn: async () => {
    const res = await fetch('/api/posts');
    if (!res.ok) throw new Error('Failed to fetch');
    return res.json();
  },
  staleTime: 1000 * 60, // Fresh for 1 minute
  gcTime: 1000 * 60 * 5, // Cache for 5 minutes
  select: (data) => data.slice(0, 10), // Transform data (optional)
});

// Data-first rendering pattern
if (data) return <PostList posts={data} />;
if (error) return <Error message={error.message} />;
return <Skeleton />;

Query Options Helper

import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';

function postOptions(id: string) {
  return queryOptions({
    queryKey: ['posts', id],
    queryFn: () => fetchPost(id),
    staleTime: 1000 * 60,
  });
}

// Usage - same options everywhere
useQuery(postOptions('123'));
useSuspenseQuery(postOptions('456'));
queryClient.prefetchQuery(postOptions('789'));
queryClient.invalidateQueries({ queryKey: postOptions('123').queryKey });

Query Key Factory (TkDodo Pattern)

For granular cache invalidation, separate keys from options:

// apps/web/src/modules/posts/hooks/post-queries.ts
export const postKeys = {
  all: ['posts'] as const,
  lists: () => [...postKeys.all, 'list'] as const,
  list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
  details: () => [...postKeys.all, 'detail'] as const,
  detail: (id: string) => [...postKeys.details(), id] as const,
};

export const postQueries = {
  list: (filters: PostFilters) =>
    queryOptions({
      queryKey: postKeys.list(filters),
      queryFn: () => getPosts({ data: filters }),
    }),
  detail: (id: string) =>
    queryOptions({
      queryKey: postKeys.detail(id),
      queryFn: () => getPost({ data: { id } }),
    }),
};

// Granular invalidation
queryClient.invalidateQueries({ queryKey: postKeys.all }); // All posts
queryClient.invalidateQueries({ queryKey: postKeys.lists() }); // All lists
queryClient.invalidateQueries({ queryKey: postKeys.detail('123') }); // One post

See Query Patterns for more key factory patterns.

Mutations

const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: async (newPost: { title: string; body: string }) => {
    const res = await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify(newPost),
    });
    return res.json();
  },
  onSuccess: async () => {
    await queryClient.invalidateQueries({ queryKey: ['posts'] });
  },
});

mutation.mutate(data);
// Or: await mutation.mutateAsync(data);

Query with Server Functions

import { createServerFn } from '@tanstack/react-start';

const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
  return await db.query.posts.findMany();
});

function postsOptions() {
  return queryOptions({
    queryKey: ['posts'],
    queryFn: () => getPosts(),
  });
}

// Prefetch in loader
export const Route = createFileRoute('/posts')({
  loader: async ({ context }) => {
    await context.queryClient.ensureQueryData(postsOptions());
  },
});

Cache Operations

const queryClient = useQueryClient();

// Invalidate
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['posts', '123'] });

// Set data directly
queryClient.setQueryData(['posts', '123'], newPost);

// Prefetch
await queryClient.prefetchQuery(postOptions('456'));

State Comparison

StateMeaning
isPendingNo data yet (first load or disabled)
isLoadingFirst load, fetching, no cached data
isFetchingAny fetch (including background refetch)
isSuccessQuery succeeded, data available
isErrorQuery failed

Common Options

OptionDefaultDescription
staleTime0Time until data is considered stale
gcTime5 minTime to keep unused data in cache
retry3Number of retry attempts
refetchOnWindowFocustrueRefetch when window regains focus
enabledtrueWhether query should execute

Forms Integration (Brief)

Two approaches to combine forms with server state:

Copy to Form State (Simple):

const { data } = useQuery(userOptions(id));
const [name, setName] = useState(data?.name ?? '');
// Form now independent of server state - loses background updates

Derived State (Advanced):

const { data } = useQuery(userOptions(id));
const [nameOverride, setNameOverride] = useState<string>();
// Show user's input if changed, otherwise server value
<input
  value={nameOverride ?? data?.name}
  onChange={(e) => setNameOverride(e.target.value)}
/>;

See tanstack-form skill for full form patterns.

Common Mistakes

MistakeCorrect Pattern
Checking isPending before dataData-first: check dataerrorisPending
Copying server state to local useStateUse data directly or derived state pattern
Creating QueryClient in componentCreate once outside component or in useState
Using refetch() for parameter changesInclude params in queryKey, let it refetch automatically
Same key for useQuery and useInfiniteQueryUse distinct key segments (different cache structures)
Inline select without memoizationExtract to stable function or useCallback
Using catch without re-throwingThrow errors in queryFn (fetch doesn't reject on 4xx/5xx)
Manual generics on useQueryType the queryFn return, let inference work
Destructuring query for type narrowingKeep query object intact for proper narrowing
Relying on deprecated onSuccess for state syncUse the data directly from useQuery
Premature render optimizationFocus on correctness first, optimize later

Delegation

  • Query pattern discovery: For finding existing query implementations, use Explore agent
  • Cache strategy review: For comprehensive cache analysis, use Task agent
  • Code review: After implementing queries, delegate to code-reviewer agent

Topic References

Core Patterns

Advanced Topics

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon