Back to list
yonatangross

tanstack-query-advanced

by yonatangross

The Complete AI Development Toolkit for Claude Code — 159 skills, 34 agents, 20 commands, 144 hooks. Production-ready patterns for FastAPI, React 19, LangGraph, security, and testing.

29🍴 4📅 Jan 23, 2026

SKILL.md


name: tanstack-query-advanced description: Advanced TanStack Query v5 patterns for infinite queries, optimistic updates, prefetching, gcTime, and queryOptions. Use when building data fetching, caching, or optimistic updates. tags: [tanstack-query, react-query, caching, infinite-scroll, optimistic-updates, prefetching, suspense] context: fork agent: frontend-ui-developer version: 1.0.0 allowed-tools: [Read, Write, Grep, Glob] author: OrchestKit user-invocable: false

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>
    </>
  );
}

2. 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] });
    },
  });
}

3. 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;
};

4. 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] });

5. 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 };
}

6. 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,
    }),
  });
}

7. 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),
    },
  },
});

8. 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>

9. 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

DecisionOption AOption BRecommendation
Query key structureStringArrayArray - supports hierarchy and serialization
Cache timingstaleTimegcTimeBoth - staleTime for freshness, gcTime for memory
Loading stateisLoadingisPendingisPending (v5) - isLoading includes background refetches
Query definitionInlinequeryOptionsqueryOptions - reusable for prefetch/loader/useQuery
SuspenseuseQuery + loadinguseSuspenseQueryuseSuspenseQuery for React 18+ Suspense
Optimistic updatessetQueryData onlysetQueryData + invalidateBoth - optimistic then reconcile
Parallel queriesMultiple useQueryuseQueriesuseQueries - combined loading/error state
Infinite queriesManual paginationuseInfiniteQueryuseInfiniteQuery - 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!
}, []);
  • 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

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

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

+20
LICENSE

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

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

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

0/5
Issue管理

オープンIssueが50未満

+5
言語

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

+5
タグ

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

+5

Reviews

💬

Reviews coming soon