| name | tanstack-query |
| description | Master TanStack Query (React Query) v5 for server state management in React applications. Use when fetching data from APIs, managing server state, caching, or handling mutations. Triggers on phrases like "react query", "tanstack query", "data fetching", "cache management", "server state", or file patterns like *query*.ts, *Query*.tsx, queryClient.ts. |
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
- 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
pnpm add @tanstack/react-query
yarn add @tanstack/react-query
2. 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>
);
}
3. 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>
);
}
4. 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: () => {
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.
useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) });
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:
queryFn: async () => {
const res = await fetch(url);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
queryFn: () => axios.get(url).then(res => res.data)
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
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
},
},
});
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60,
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,
});
Query Invalidation
Mark queries as stale to trigger refetches:
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: ['todos'] });
queryClient.invalidateQueries({ queryKey: ['todo', todoId] });
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'active'
});
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');
},
});
mutation.mutate({ title: 'New Todo' });
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() {
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
function App() {
return (
<Suspense fallback={<div>Loading todos...</div>}>
<TodoList />
</Suspense>
);
}
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
Common Patterns
Dependent Queries
Run queries in sequence when one depends on another:
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user.id),
enabled: !!user?.id,
});
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>;
}
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();
function TodoListLink({ id }) {
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
staleTime: 1000 * 60 * 5,
});
};
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,
staleTime: 1000 * 60,
});
return <div>{data.title}</div>;
}
Placeholder Data
Show placeholder while loading:
const { data, isPlaceholderData } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: (previousData) => previousData,
});
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
placeholderData: { items: [], total: 0 },
});
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
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
console.error('Query error:', error);
},
},
mutations: {
onError: (error) => {
console.error('Mutation error:', 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,
});
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: