TanStack Query (React Query)
Powerful server state management with caching, background updates, and more
TanStack Query - Server State Management
TanStack Query (formerly React Query) is a powerful data-fetching and server state management library. It handles caching, background updates, stale data, pagination, and more out of the box. It's specifically designed for managing server state - data that lives on your backend.
Key Features
- Automatic Caching — Cache responses and reduce API calls
- Background Updates — Refetch stale data automatically
- Request Deduplication — Dedupe identical concurrent requests
- Optimistic Updates — Update UI before server confirms
- Infinite Queries — Easy pagination and infinite scroll
Installation
npm install @tanstack/react-query
Setup
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
retry: 3,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Basic Query
import { useQuery } from '@tanstack/react-query';
// Fetch function
async function fetchTodos() {
const response = await fetch('/api/todos');
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
}
function TodoList() {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['todos'], // Unique key for caching
queryFn: fetchTodos, // Function that returns a promise
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Query with Parameters
import { useQuery } from '@tanstack/react-query';
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId], // Key includes the parameter
queryFn: () => fetchUser(userId),
enabled: !!userId, // Only run if userId exists
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Dependent queries
function UserPosts({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user.id),
enabled: !!user?.id, // Only fetch when user is loaded
});
return (/* ... */);
}
Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query';
async function createTodo(newTodo) {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
});
return response.json();
}
function AddTodo() {
const queryClient = useQueryClient();
const [text, setText] = useState('');
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
// Invalidate and refetch todos
queryClient.invalidateQueries({ queryKey: ['todos'] });
setText('');
},
onError: (error) => {
console.error('Failed to create todo:', error);
},
});
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ text, completed: false });
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
disabled={mutation.isPending}
/>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
);
}
Optimistic Updates
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoItem({ todo }) {
const queryClient = useQueryClient();
const toggleMutation = useMutation({
mutationFn: (todoId) => fetch(`/api/todos/${todoId}/toggle`, { method: 'PATCH' }),
// Optimistic update
onMutate: async (todoId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old) =>
old.map(t => t.id === todoId ? { ...t, completed: !t.completed } : t)
);
// Return context with snapshot
return { previousTodos };
},
// If mutation fails, rollback
onError: (err, todoId, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<li onClick={() => toggleMutation.mutate(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
);
}
Infinite Queries
import { useInfiniteQuery } from '@tanstack/react-query';
async function fetchPosts({ pageParam = 1 }) {
const response = await fetch(`/api/posts?page=${pageParam}&limit=10`);
return response.json();
}
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage, pages) => {
// Return undefined if no more pages
return lastPage.hasMore ? pages.length + 1 : undefined;
},
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'No more posts'}
</button>
</div>
);
}
Query States
| State | Description |
|---|---|
isPending |
Query has no data yet (first load) |
isLoading |
isPending && isFetching (initial load) |
isFetching |
Any fetch is in progress (including background) |
isStale |
Data is older than staleTime |
isSuccess |
Query has data and no error |
💡 Best Practices
- • Use descriptive query keys: ['todos', { filter, sort }]
- • Set appropriate staleTime for your data freshness needs
- • Use enabled option to control when queries run
- • Invalidate queries after mutations
- • Use optimistic updates for better UX
- • Install React Query DevTools for debugging