tanstack-query-advanced

TanStack Query Advanced

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-advanced" with this command: npx skills add yonatangross/orchestkit/yonatangross-orchestkit-tanstack-query-advanced

TanStack Query Advanced

Production patterns for TanStack Query v5 - server state management done right.

Overview

  • Infinite scroll / pagination

  • Optimistic UI updates

  • Prefetching for instant navigation

  • Complex cache invalidation

  • Dependent/parallel queries

  • Mutations with rollback

Core Patterns

  1. Infinite Queries (Cursor-Based)

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

interface Page { items: Item[]; nextCursor: string | null; }

function useInfiniteItems() { return useInfiniteQuery({ queryKey: ['items'], queryFn: async ({ pageParam }): Promise<Page> => { const res = await fetch(/api/items?cursor=${pageParam ?? ''}); return res.json(); }, initialPageParam: null as string | null, getNextPageParam: (lastPage) => lastPage.nextCursor, getPreviousPageParam: (firstPage) => firstPage.prevCursor, }); }

// Component function ItemList() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteItems();

return ( <> {data?.pages.flatMap((page) => page.items.map((item) => ( <ItemCard key={item.id} item={item} /> )))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'} </button> </> ); }

  1. Optimistic Updates

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

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

return useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });

  // Snapshot previous value
  const previousTodo = queryClient.getQueryData(['todos', newTodo.id]);

  // Optimistically update
  queryClient.setQueryData(['todos', newTodo.id], newTodo);

  // Return context for rollback
  return { previousTodo };
},
onError: (err, newTodo, context) => {
  // Rollback on error
  queryClient.setQueryData(['todos', newTodo.id], context?.previousTodo);
},
onSettled: (data, error, variables) => {
  // Always refetch after error or success
  queryClient.invalidateQueries({ queryKey: ['todos', variables.id] });
},

}); }

  1. Prefetching Patterns

// Prefetch on hover function UserLink({ userId }: { userId: string }) { const queryClient = useQueryClient();

const prefetchUser = () => { queryClient.prefetchQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 5 * 60 * 1000, // 5 minutes }); };

return ( <Link to={/users/${userId}} onMouseEnter={prefetchUser}> View User </Link> ); }

// Prefetch in loader (React Router) export const loader = (queryClient: QueryClient) => async ({ params }) => { await queryClient.ensureQueryData({ queryKey: ['user', params.id], queryFn: () => fetchUser(params.id), }); return null; };

  1. Smart Cache Invalidation

const queryClient = useQueryClient();

// Invalidate exact query queryClient.invalidateQueries({ queryKey: ['todos', 1] });

// Invalidate all todos queries queryClient.invalidateQueries({ queryKey: ['todos'] });

// Invalidate with predicate queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && (query.queryKey[1] as Todo)?.status === 'done', });

// Invalidate and refetch immediately queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' });

// Remove from cache entirely queryClient.removeQueries({ queryKey: ['todos', 1] });

  1. Dependent Queries

function useUserPosts(userId: string) { // First query const userQuery = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), });

// Dependent query - only runs when user is loaded const postsQuery = useQuery({ queryKey: ['posts', userId], queryFn: () => fetchUserPosts(userId), enabled: !!userQuery.data, // Only fetch when user exists });

return { user: userQuery.data, posts: postsQuery.data }; }

  1. Parallel Queries

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

function useMultipleUsers(userIds: string[]) { return useQueries({ queries: userIds.map((id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id), staleTime: 5 * 60 * 1000, })), combine: (results) => ({ users: results.map((r) => r.data).filter(Boolean), pending: results.some((r) => r.isPending), error: results.find((r) => r.error)?.error, }), }); }

  1. Query Deduplication & Batching

// Configure in QueryClient const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, // 1 minute gcTime: 1000 * 60 * 5, // 5 minutes (formerly cacheTime) refetchOnWindowFocus: false, retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }, }, });

  1. Suspense Integration

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

function UserProfile({ userId }: { userId: string }) { // This will suspend until data is ready const { data: user } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), });

return <div>{user.name}</div>; }

// Wrap with Suspense <Suspense fallback={<Skeleton />}> <UserProfile userId="123" /> </Suspense>

  1. Mutation State Tracking

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

function PendingTodos() { // Track all pending todo mutations const pendingMutations = useMutationState({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables as Todo, });

return ( <> {pendingMutations.map((todo) => ( <TodoItem key={todo.id} todo={todo} isPending /> ))} </> ); }

Configuration Best Practices

const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, // Data fresh for 1 min gcTime: 1000 * 60 * 5, // Cache for 5 min refetchOnWindowFocus: true, // Refetch on tab focus refetchOnReconnect: true, // Refetch on network reconnect retry: 3, // Retry failed requests }, mutations: { retry: 1, onError: (error) => toast.error(error.message), }, }, });

Quick Reference

// ✅ Create typed query with queryOptions helper (v5) const userQueryOptions = (id: string) => queryOptions({ queryKey: ['user', id] as const, queryFn: () => fetchUser(id), staleTime: 5 * 60 * 1000, });

// ✅ Use the query options for consistency const { data } = useQuery(userQueryOptions(userId)); await queryClient.prefetchQuery(userQueryOptions(userId)); await queryClient.ensureQueryData(userQueryOptions(userId));

// ✅ v5: isPending instead of isLoading (initial load only) if (isPending) return <Skeleton />;

// ✅ v5: gcTime instead of cacheTime gcTime: 5 * 60 * 1000,

// ✅ useSuspenseQuery for Suspense integration const { data } = useSuspenseQuery(userQueryOptions(userId));

// ✅ Selective invalidation queryClient.invalidateQueries({ queryKey: ['todos'], exact: true });

// ❌ NEVER destructure useQuery result at call site const { data, isLoading } = useQuery({ queryKey: ['users'] }); // BAD - recreates object

// ❌ NEVER use string keys useQuery({ queryKey: 'users' }); // BAD - use arrays

// ❌ NEVER store server state in Zustand const useStore = create((set) => ({ users: [] })); // BAD - use React Query

Key Decisions

Decision Option A Option B Recommendation

Query key structure String Array Array - supports hierarchy and serialization

Cache timing staleTime gcTime Both - staleTime for freshness, gcTime for memory

Loading state isLoading isPending isPending (v5) - isLoading includes background refetches

Query definition Inline queryOptions queryOptions - reusable for prefetch/loader/useQuery

Suspense useQuery + loading useSuspenseQuery useSuspenseQuery for React 18+ Suspense

Optimistic updates setQueryData only setQueryData + invalidate Both - optimistic then reconcile

Parallel queries Multiple useQuery useQueries useQueries - combined loading/error state

Infinite queries Manual pagination useInfiniteQuery useInfiniteQuery - built-in cursor handling

Anti-Patterns (FORBIDDEN)

// ❌ FORBIDDEN: Storing server state in Zustand/Redux const useStore = create((set) => ({ users: [], // Server state belongs in React Query! fetchUsers: async () => { const users = await api.getUsers(); set({ users }); // Stale data, no background refetch }, }));

// ❌ FORBIDDEN: String query keys useQuery({ queryKey: 'todos', // Must be an array! queryFn: fetchTodos, });

// ❌ FORBIDDEN: Using deprecated cacheTime (v5) useQuery({ queryKey: ['todos'], cacheTime: 5 * 60 * 1000, // WRONG - use gcTime in v5 });

// ❌ FORBIDDEN: Using isLoading for initial state (v5) // isLoading = isPending && isFetching (includes background refetch) if (isLoading) return <Skeleton />; // WRONG - use isPending

// ❌ FORBIDDEN: Over-invalidating after mutations useMutation({ mutationFn: updateTodo, onSuccess: () => { queryClient.invalidateQueries(); // Invalidates EVERYTHING! }, });

// ❌ FORBIDDEN: Forgetting to cancel queries in optimistic updates useMutation({ onMutate: async (newTodo) => { // Missing: await queryClient.cancelQueries(...) const previous = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], (old) => [...old, newTodo]); return { previous }; }, });

// ❌ FORBIDDEN: Not returning context from onMutate useMutation({ onMutate: async (newTodo) => { const previous = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], (old) => [...old, newTodo]); // Missing: return { previous }; // Required for rollback! }, onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context?.previous); // context is undefined! }, });

// ❌ FORBIDDEN: Mutating cache data directly queryClient.setQueryData(['todos'], (old) => { old.push(newTodo); // WRONG - mutates existing array return old; }); // ✅ CORRECT: Return new array queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);

// ❌ FORBIDDEN: Fetching inside useEffect useEffect(() => { fetch('/api/users').then(setUsers); // Use React Query instead! }, []);

Related Skills

  • zustand-patterns

  • Client state management (use alongside React Query for server state)

  • form-state-patterns

  • Form state with React Hook Form (integrate mutation status)

  • msw-mocking

  • Mock Service Worker for testing queries without network

  • react-server-components-framework

  • RSC hydration with React Query

Capability Details

infinite-queries

Keywords: infinite, pagination, cursor, load more, scroll, pages Solves: Implementing cursor-based pagination with automatic page management

optimistic-updates

Keywords: optimistic, instant, rollback, onMutate, setQueryData, cancel Solves: Showing immediate UI feedback before server confirmation with rollback

prefetching

Keywords: prefetch, hover, preload, ensureQueryData, loader, navigation Solves: Loading data before it's needed for instant navigation

cache-invalidation

Keywords: invalidate, refetch, stale, fresh, gcTime, staleTime, exact Solves: Keeping cache in sync with server after mutations

suspense-integration

Keywords: suspense, useSuspenseQuery, streaming, fallback, boundary Solves: Integrating with React Suspense for declarative loading states

parallel-queries

Keywords: useQueries, parallel, concurrent, combine, batch Solves: Fetching multiple independent queries with combined state

References

  • references/cache-strategies.md

  • Cache invalidation patterns

  • scripts/query-hooks-template.ts

  • Production query hook template

  • checklists/tanstack-checklist.md

  • Implementation checklist

  • examples/tanstack-examples.md

  • Real-world usage examples

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

responsive-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

domain-driven-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

dashboard-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

rag-retrieval

No summary provided by upstream source.

Repository SourceNeeds Review