Hooks for managing, caching and syncing asynchronous and remote data in React
@tanstack/react-query is a specialized library for managing server state in React applications. Unlike client state (UI toggles, form inputs), server state represents data that lives on a remote server and must be fetched, cached, and synchronized. React doesn't provide built-in opinions on data fetching, leaving developers to wire up useEffect hooks with manual cache management, loading states, and error handling. TanStack Query solves this by providing a declarative API that handles the entire lifecycle of asynchronous data.
The library revolves around queries (for fetching data) and mutations (for modifying data). Each query is identified by a unique key and associated with a function that returns a Promise. TanStack Query automatically manages caching, background refetching, stale data invalidation, and request deduplication. It also provides powerful features like optimistic updates, infinite scrolling, prefetching, and automatic retry logic. With over 16 million weekly downloads, it's become the de facto standard for data fetching in React applications.
Originally called React Query, the package was rebranded to TanStack Query when the team expanded support to other frameworks (Vue, Svelte, Solid). The React implementation remains the most popular variant. Version 5 is the current major release, offering improved TypeScript support and a more streamlined API. Developers typically see significant reductions in boilerplate code compared to manual fetch implementations, while gaining robust handling of edge cases like race conditions and component unmounting.
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
retry: 1,
},
},
});
function TodoList() {
const queryClient = useQueryClient();
const [filter, setFilter] = useState('all');
const { data: todos, isPending, error } = useQuery({
queryKey: ['todos', filter],
queryFn: async () => {
const response = await fetch(`/api/todos?filter=${filter}`);
if (!response.ok) throw new Error('Failed to fetch todos');
return response.json();
},
});
const toggleMutation = useMutation({
mutationFn: async (id) => {
const response = await fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' });
return response.json();
},
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos', filter]);
queryClient.setQueryData(['todos', filter], (old) =>
old.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo)
);
return { previousTodos };
},
onError: (err, id, context) => {
queryClient.setQueryData(['todos', filter], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
if (isPending) return <div>Loading todos...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleMutation.mutate(todo.id)}
/>
{todo.title}
</li>
))}
</ul>
</div>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
);
}Dashboard applications with real-time data: Build admin panels or monitoring dashboards where multiple widgets need to fetch and refresh data independently. TanStack Query's automatic background refetching ensures users see up-to-date information without manual refresh buttons, while intelligent caching prevents redundant network requests.
E-commerce product catalogs with filtering: Implement product listing pages where users filter by category, price, or availability. Each filter combination creates a unique query key, allowing instant display of cached results when users toggle back to previous filters. Prefetching adjacent pages during idle time improves perceived performance.
Social media feeds with infinite scroll: Load paginated content where users scroll through potentially endless lists of posts or comments. The useInfiniteQuery hook manages page parameters automatically, appending new data to existing results while maintaining proper loading states and error handling for each page.
Form-heavy applications with dependent fields: Build complex forms where dropdown options depend on previous selections (e.g., country → state → city). React Query's enabled option conditionally triggers queries, and mutation callbacks update related queries after successful submissions without full page reloads.
Multi-tenant SaaS applications: Handle data fetching across different customer accounts or workspaces where switching contexts requires loading new datasets. Query keys scoped to tenant IDs ensure proper cache isolation, and query invalidation on context switches prevents stale data from appearing.
npm install @tanstack/react-querypnpm add @tanstack/react-querybun add @tanstack/react-query