tanstack-query

TanStack Query v5 patterns for server state management, caching, mutations, optimistic updates, and query organization. Use when working with TanStack Query, React Query, server state, data fetching hooks, or when the user asks about caching strategies, query invalidation, or mutation patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "tanstack-query" with this command: npx skills add grahamcrackers/skills/grahamcrackers-skills-tanstack-query

TanStack Query v5 Best Practices

Core Principles

  • TanStack Query manages server state — async data from APIs, databases, etc. Don't use it for client-only state (forms, UI toggles, modals).
  • All hooks use a single object signature (v5 breaking change):
useQuery({ queryKey, queryFn, ...options });
useMutation({ mutationFn, ...options });

Query Keys

Design query keys as hierarchical arrays for granular invalidation:

const queryKeys = {
    users: {
        all: ["users"] as const,
        lists: () => [...queryKeys.users.all, "list"] as const,
        list: (filters: UserFilters) => [...queryKeys.users.lists(), filters] as const,
        details: () => [...queryKeys.users.all, "detail"] as const,
        detail: (id: string) => [...queryKeys.users.details(), id] as const,
    },
} as const;

Use a query key factory per domain entity. This enables:

  • queryClient.invalidateQueries({ queryKey: queryKeys.users.all }) — invalidate everything user-related.
  • queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() }) — invalidate only lists.

Custom Query Hooks

Wrap useQuery in custom hooks — never call useQuery directly in components:

function useUser(id: string) {
    return useQuery({
        queryKey: queryKeys.users.detail(id),
        queryFn: () => api.users.getById(id),
        staleTime: 5 * 60 * 1000,
    });
}

function useUsers(filters: UserFilters) {
    return useQuery({
        queryKey: queryKeys.users.list(filters),
        queryFn: () => api.users.list(filters),
    });
}

This collocates query configuration, makes queries reusable, and provides a single place to update options.

Important Defaults

Understand the defaults before overriding them:

DefaultValueNotes
staleTime0Data is immediately stale; refetches on mount/focus/reconnect
gcTime5 minInactive cache entries garbage collected after 5 minutes
retry3Failed queries retry 3 times with exponential backoff
refetchOnWindowFocustrueStale queries refetch when window regains focus
structuralSharingtrueResults are referentially stable if data hasn't changed

Set staleTime appropriately for your data — data that rarely changes can have staleTime: Infinity.

Mutations

Basic Mutation Hook

function useCreateUser() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: (data: CreateUserInput) => api.users.create(data),
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() });
        },
    });
}

Direct Cache Updates

When the mutation response contains the updated data, update the cache directly instead of refetching:

function useUpdateUser() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: ({ id, data }: { id: string; data: UpdateUserInput }) => api.users.update(id, data),
        onSuccess: (updatedUser) => {
            queryClient.setQueryData(queryKeys.users.detail(updatedUser.id), updatedUser);
            queryClient.invalidateQueries({ queryKey: queryKeys.users.lists() });
        },
    });
}

Always use immutable updates with setQueryData — spread or structuredClone, never mutate in place.

Optimistic Updates

function useToggleTodo() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: (id: string) => api.todos.toggle(id),
        onMutate: async (id) => {
            await queryClient.cancelQueries({ queryKey: queryKeys.todos.detail(id) });
            const previous = queryClient.getQueryData(queryKeys.todos.detail(id));

            queryClient.setQueryData(queryKeys.todos.detail(id), (old: Todo) => ({
                ...old,
                completed: !old.completed,
            }));

            return { previous };
        },
        onError: (_err, id, context) => {
            queryClient.setQueryData(queryKeys.todos.detail(id), context?.previous);
        },
        onSettled: (_data, _err, id) => {
            queryClient.invalidateQueries({ queryKey: queryKeys.todos.detail(id) });
        },
    });
}

Dependent Queries

Use enabled to defer queries until prerequisites are available:

function useUserPosts(userId: string | undefined) {
    return useQuery({
        queryKey: queryKeys.posts.byUser(userId!),
        queryFn: () => api.posts.getByUser(userId!),
        enabled: !!userId,
    });
}

Infinite Queries

function useInfiniteUsers() {
    return useInfiniteQuery({
        queryKey: queryKeys.users.lists(),
        queryFn: ({ pageParam }) => api.users.list({ cursor: pageParam }),
        initialPageParam: undefined as string | undefined,
        getNextPageParam: (lastPage) => lastPage.nextCursor,
    });
}

Suspense Integration

function useUserSuspense(id: string) {
    return useSuspenseQuery({
        queryKey: queryKeys.users.detail(id),
        queryFn: () => api.users.getById(id),
    });
}

useSuspenseQuery guarantees data is never undefined — no need for loading/error checks in the component. Wrap the parent in <Suspense> and <ErrorBoundary>.

Query Organization

src/
├── api/              # API client functions (no TanStack Query here)
│   └── users.ts
├── queries/          # query hooks and key factories
│   ├── keys.ts       # all query key factories
│   ├── users.ts      # useUser, useUsers, useCreateUser, etc.
│   └── posts.ts

Keep API functions pure (return promises), and keep TanStack Query hooks in a separate layer. This makes the API layer testable without TanStack Query and keeps query hooks thin.

Anti-Patterns to Avoid

  • Don't use useEffect to sync query data into local state — use the query result directly.
  • Don't duplicate server state in useState — TanStack Query is your cache.
  • Don't invalidate everything — use specific query keys to invalidate only what changed.
  • Don't forget enabled: false when a query depends on runtime data that might be undefined.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tanstack-query

No summary provided by upstream source.

Repository SourceNeeds Review
General

bulletproof-react-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

git-conventions

No summary provided by upstream source.

Repository SourceNeeds Review