tanstack-query

TanStack Query (React Query) v5

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 tenequm/claude-plugins/tenequm-claude-plugins-tanstack-query

TanStack Query (React Query) v5

Powerful asynchronous state management for React. TanStack Query makes fetching, caching, synchronizing, and updating server state in your React applications a breeze.

When to Use This Skill

  • Fetching data from REST APIs or GraphQL endpoints

  • Managing server state and cache lifecycle

  • Implementing mutations (create, update, delete operations)

  • Building infinite scroll or load-more patterns

  • Handling optimistic UI updates

  • Rendering streaming/chunked data from AI or SSE endpoints

  • Integrating with tRPC v11 queryOptions pattern

  • Synchronizing data across components

  • Implementing background data refetching

  • Managing complex async state without Redux or other state managers

Quick Start Workflow

  1. Installation

npm install @tanstack/react-query

or

pnpm add @tanstack/react-query

or

yarn add @tanstack/react-query

  1. Setup QueryClient

Wrap your application with QueryClientProvider :

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ); }

  1. Basic Query

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

function TodoList() { const { data, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: async () => { const res = await fetch('https://api.example.com/todos'); if (!res.ok) throw new Error('Network response was not ok'); return res.json(); }, });

if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;

return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }

  1. Basic Mutation

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

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

const mutation = useMutation({ mutationFn: async (newTodo) => { const res = await fetch('https://api.example.com/todos', { method: 'POST', body: JSON.stringify(newTodo), headers: { 'Content-Type': 'application/json' }, }); return res.json(); }, onSuccess: () => { // Invalidate and refetch todos queryClient.invalidateQueries({ queryKey: ['todos'] }); }, });

return ( <button onClick={() => mutation.mutate({ title: 'New Todo' })}> {mutation.isPending ? 'Creating...' : 'Create Todo'} </button> ); }

Core Concepts

Query Keys

Query keys uniquely identify queries and are used for caching. They must be arrays.

// Simple key useQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Key with variables useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) });

// Hierarchical keys useQuery({ queryKey: ['todos', 'list', { filters, page }], queryFn: fetchTodos });

Query key matching:

  • ['todos']

  • exact match

  • ['todos', { page: 1 }]

  • exact match with object

  • { queryKey: ['todos'] }

  • matches all queries starting with 'todos'

Query Functions

Query functions must return a promise that resolves data or throws an error:

// Using fetch queryFn: async () => { const res = await fetch(url); if (!res.ok) throw new Error('Failed to fetch'); return res.json(); }

// Using axios queryFn: () => axios.get(url).then(res => res.data)

// With query key access queryFn: ({ queryKey }) => { const [_, todoId] = queryKey; return fetchTodo(todoId); }

Important Defaults

Understanding defaults is crucial for optimal usage:

  • staleTime: 0 - Queries become stale immediately by default

  • gcTime: 5 minutes - Unused/inactive cache data remains in memory for 5 minutes

  • retry: 3 - Failed queries retry 3 times with exponential backoff

  • refetchOnWindowFocus: true - Queries refetch when window regains focus

  • refetchOnReconnect: true - Queries refetch when network reconnects

// Override defaults globally const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 10, // 10 minutes }, }, });

// Or per query useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 1000 * 60, // 1 minute retry: 5, });

Query Status and Fetch Status

Queries have two important states:

Query Status:

  • pending

  • No cached data, query is executing

  • error

  • Query encountered an error

  • success

  • Query succeeded and data is available

Fetch Status:

  • fetching

  • Query function is executing

  • paused

  • Query wants to fetch but is paused (offline)

  • idle

  • Query is not fetching

const { data, status, fetchStatus, isLoading, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, });

// isLoading = status === 'pending' // isFetching = fetchStatus === 'fetching'

Query Invalidation

Mark queries as stale to trigger refetches:

const queryClient = useQueryClient();

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

// Invalidate specific query queryClient.invalidateQueries({ queryKey: ['todo', todoId] });

// Invalidate and refetch immediately queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'active' // only refetch active queries });

Mutations

Mutations are used for creating, updating, or deleting data:

const mutation = useMutation({ mutationFn: (newTodo) => { return fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo), }); }, onSuccess: (data, variables, context) => { console.log('Success!', data); }, onError: (error, variables, context) => { console.error('Error:', error); }, onSettled: (data, error, variables, context) => { console.log('Mutation finished'); }, });

// Trigger mutation mutation.mutate({ title: 'New Todo' });

// With async/await mutation.mutateAsync({ title: 'New Todo' }) .then(data => console.log(data)) .catch(error => console.error(error));

React Suspense Integration

TanStack Query supports React Suspense with dedicated hooks:

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

function TodoList() { // This will suspend the component until data is ready const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos, });

// No need for loading states - handled by Suspense boundary return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }

// In parent component function App() { return ( <Suspense fallback={<div>Loading todos...</div>}> <TodoList /> </Suspense> ); }

Streamed Queries (Experimental)

Consume AsyncIterable streams as query data - ideal for AI chat, SSE, and streaming responses:

import { useQuery, queryOptions } from '@tanstack/react-query'; import { experimental_streamedQuery as streamedQuery } from '@tanstack/react-query';

async function* fetchChatStream(sessionId: string): AsyncIterable<string> { const response = await fetch(/api/chat/${sessionId}, { method: 'POST' }); const reader = response.body!.getReader(); const decoder = new TextDecoder();

while (true) { const { done, value } = await reader.read(); if (done) break; yield decoder.decode(value); } }

function ChatMessages({ sessionId }: { sessionId: string }) { const { data: chunks, status, fetchStatus } = useQuery( queryOptions({ queryKey: ['chat', sessionId], queryFn: streamedQuery({ streamFn: () => fetchChatStream(sessionId), // Optional: customize how chunks accumulate // reducer: (acc, chunk) => [...acc, chunk], // initialValue: [], refetchMode: 'reset', // 'reset' | 'append' | 'replace' }), }) );

// status === 'pending' until first chunk arrives // status === 'success' after first chunk, fetchStatus === 'fetching' until stream ends if (status === 'pending') return <div>Waiting for response...</div>;

return ( <div> {chunks?.map((chunk, i) => <span key={i}>{chunk}</span>)} {fetchStatus === 'fetching' && <span className="cursor" />} </div> ); }

refetchMode options:

  • 'reset'

  • clear data and start fresh on refetch

  • 'append'

  • keep existing chunks and add new ones

  • 'replace'

  • replace data chunk-by-chunk on refetch

Note: The API stabilized at v5.86.0. Earlier versions used queryFn instead of streamFn and maxChunks instead of reducer .

Prefetch in Render (Experimental)

Use React 19's React.use() with TanStack Query for "render-as-you-fetch":

// Enable the feature flag const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, });

// Component that suspends with React.use() function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise); // Suspends until resolved return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>; }

function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }); return ( <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> ); }

Known limitations: queries may run twice on unsuspend, incompatible with useQueries , skipToken and refetch() cannot be used together.

Advanced Topics

For detailed information on advanced patterns, see the reference files:

Infinite Queries

For implementing infinite scroll and load-more patterns:

  • See references/infinite-queries.md for comprehensive guide

  • Covers useInfiniteQuery hook

  • Bidirectional pagination

  • getNextPageParam and getPreviousPageParam

  • Refetching and background updates

Optimistic Updates

For updating UI before server confirmation:

  • See references/optimistic-updates.md for detailed patterns

  • Optimistic mutations

  • Rollback on error

  • Context for cancellation

  • UI feedback strategies

TypeScript Support

For full type safety and inference:

  • See references/typescript.md for complete TypeScript guide

  • Type inference from query functions

  • Generic type parameters

  • Typing query options

  • Custom hooks with types

  • Error type narrowing

Query Invalidation Patterns

For advanced cache invalidation strategies:

  • See references/query-invalidation.md

  • Partial matching

  • Predicate functions

  • Refetch strategies

  • Query filters

Performance Optimization

For optimizing query performance:

  • See references/performance.md

  • Query deduplication

  • Structural sharing

  • Memory management

  • Query splitting strategies

DevTools

TanStack Query DevTools provide visual insights into query state:

npm install @tanstack/react-query-devtools

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

function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }

DevTools features:

  • View all queries and their states

  • Inspect query data and errors

  • Manually trigger refetches

  • Invalidate queries

  • Monitor cache lifecycle

  • Visual indicator for staleTime: Infinity ("static") queries (v5.80.0+)

Common Patterns

Dependent Queries

Run queries in sequence when one depends on another:

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

// Second query depends on first const { data: projects } = useQuery({ queryKey: ['projects', user?.id], queryFn: () => fetchProjects(user.id), enabled: !!user?.id, // Only run when user.id is available });

Parallel Queries

Multiple independent queries in one component:

function Dashboard() { const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers }); const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts }); const stats = useQuery({ queryKey: ['stats'], queryFn: fetchStats });

if (users.isLoading || posts.isLoading || stats.isLoading) { return <div>Loading...</div>; }

// All queries succeeded return <DashboardView users={users.data} posts={posts.data} stats={stats.data} />; }

Dynamic Parallel Queries

Use useQueries for dynamic number of queries:

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

function TodoLists({ listIds }) { const results = useQueries({ queries: listIds.map((id) => ({ queryKey: ['list', id], queryFn: () => fetchList(id), })), });

const isLoading = results.some(result => result.isLoading); const data = results.map(result => result.data);

return <Lists data={data} />; }

Prefetching

Prefetch data before it's needed:

const queryClient = useQueryClient();

// Prefetch on hover function TodoListLink({ id }) { const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id), staleTime: 1000 * 60 * 5, // Cache for 5 minutes }); };

return ( <Link to={/todo/${id}} onMouseEnter={prefetch}> View Todo </Link> ); }

Initial Data

Provide initial data to avoid loading states:

function TodoDetail({ todoId, initialTodo }) { const { data } = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId), initialData: initialTodo, // Use this data immediately staleTime: 1000 * 60, // Consider fresh for 1 minute });

return <div>{data.title}</div>; }

Placeholder Data

Show placeholder while loading:

const { data, isPlaceholderData } = useQuery({ queryKey: ['todos', page], queryFn: () => fetchTodos(page), placeholderData: (previousData) => previousData, // Keep previous data while loading });

// Or use static placeholder const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, placeholderData: { items: [], total: 0 }, });

// TypeScript: isPlaceholderData now narrows data type (v5.65.0+) // When isPlaceholderData is true, data is typed as the placeholder type

tRPC v11 Integration

tRPC v11 exposes queryOptions and mutationOptions directly, removing the need for custom hook wrappers:

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

function TodoList() { const trpc = useTRPC(); const queryClient = useQueryClient();

// Direct queryOptions pattern (replaces trpc.todo.list.useQuery()) const { data } = useQuery(trpc.todo.list.queryOptions());

const createTodo = useMutation( trpc.todo.create.mutationOptions({ onSuccess: () => { queryClient.invalidateQueries(trpc.todo.list.queryOptions()); }, }) );

// Prefetching also works queryClient.prefetchQuery(trpc.todo.list.queryOptions()); }

Requires @tanstack/react-query@5.62.8+ and @trpc/tanstack-react-query .

Error Handling

Query Errors

const { error, isError } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, retry: 3, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), });

if (isError) { return <div>Error: {error.message}</div>; }

Global Error Handling

Use QueryCache and MutationCache callbacks for global error handling:

import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';

const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error, query) => { console.error(Query ${query.queryKey} failed:, error); // Show toast notification, etc. }, }), mutationCache: new MutationCache({ onError: (error, _variables, _context, mutation) => { console.error('Mutation failed:', error); }, }), });

Error Boundaries

Combine with React Error Boundaries:

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

function TodoList() { const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, throwOnError: true, // Throw errors to error boundary });

return <div>{/* render data */}</div>; }

function App() { return ( <ErrorBoundary fallback={<div>Something went wrong</div>}> <TodoList /> </ErrorBoundary> ); }

Best Practices

Use Query Keys Wisely

  • Structure keys hierarchically: ['todos', 'list', { filters }]

  • Include all variables in the key

  • Keep keys consistent across your app

Set Appropriate staleTime

  • Static data: staleTime: Infinity

  • Frequently changing: staleTime: 0 (default)

  • Moderately changing: staleTime: 1000 * 60 * 5 (5 minutes)

Handle Loading and Error States

  • Always check isLoading and error

  • Provide meaningful loading indicators

  • Show user-friendly error messages

Optimize Refetching

  • Disable unnecessary refetches with refetchOnWindowFocus: false

  • Use staleTime to reduce refetches

  • Consider using refetchInterval for polling

Invalidate Efficiently

  • Invalidate specific queries, not all queries

  • Use query key prefixes for related queries

  • Combine with optimistic updates for better UX

Use TypeScript

  • Type your query functions for type inference

  • Use generic type parameters when needed

  • Enable strict type checking

Leverage DevTools

  • Install DevTools in development

  • Monitor query behavior

  • Debug cache issues

Resources

Migration from v4

If you're upgrading from React Query v4:

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

chrome-extension-wxt

No summary provided by upstream source.

Repository SourceNeeds Review
General

founder-playbook

No summary provided by upstream source.

Repository SourceNeeds Review
General

skill-finder

No summary provided by upstream source.

Repository SourceNeeds Review