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
- 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
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: () => {
// 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 executingerror- Query encountered an errorsuccess- Query succeeded and data is available
Fetch Status:
fetching- Query function is executingpaused- 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>
);
}
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.mdfor comprehensive guide - Covers
useInfiniteQueryhook - Bidirectional pagination
getNextPageParamandgetPreviousPageParam- Refetching and background updates
Optimistic Updates
For updating UI before server confirmation:
- See
references/optimistic-updates.mdfor detailed patterns - Optimistic mutations
- Rollback on error
- Context for cancellation
- UI feedback strategies
TypeScript Support
For full type safety and inference:
- See
references/typescript.mdfor 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:
// 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 },
});
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);
// Show toast notification, etc.
},
},
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, // 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
- Structure keys hierarchically:
-
Set Appropriate staleTime
- Static data:
staleTime: Infinity - Frequently changing:
staleTime: 0(default) - Moderately changing:
staleTime: 1000 * 60 * 5(5 minutes)
- Static data:
-
Handle Loading and Error States
- Always check
isLoadinganderror - Provide meaningful loading indicators
- Show user-friendly error messages
- Always check
-
Optimize Refetching
- Disable unnecessary refetches with
refetchOnWindowFocus: false - Use
staleTimeto reduce refetches - Consider using
refetchIntervalfor polling
- Disable unnecessary refetches with
-
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
- Official Documentation: https://tanstack.com/query/latest/docs/framework/react/overview
- GitHub Repository: https://github.com/TanStack/query
- Examples: https://tanstack.com/query/latest/docs/framework/react/examples
- Community: https://discord.gg/tanstack
- TypeScript Guide: https://tanstack.com/query/latest/docs/framework/react/typescript
Migration from v4
If you're upgrading from React Query v4:
cacheTimerenamed togcTimeuseInfiniteQuerypageParam changes- New
useSuspenseQueryhooks - Improved TypeScript inference
- See official migration guide: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5