On this page
TanStack Query
TanStack Query (formerly React Query) handles fetching, caching, synchronizing, and updating server state in React applications.
Setup
npm install @tanstack/react-query
// main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
retry: 1
}
}
});
ReactDOM.createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
Basic Query
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
enabled: !!userId // Only run when userId exists
});
if (isLoading) return <Spinner />;
if (isError) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{data.name}</h1>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateTodoForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo)
}).then(r => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ text: e.target.text.value });
};
return (
<form onSubmit={handleSubmit}>
<input name="text" disabled={mutation.isPending} />
<button type="submit">
{mutation.isPending ? 'Adding...' : 'Add'}
</button>
{mutation.isError && <p>{mutation.error.message}</p>}
</form>
);
}
Optimistic Updates
Update UI immediately, rollback on error:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (updatedTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) =>
old.map(t => t.id === updatedTodo.id ? updatedTodo : t)
);
return { previous };
},
onError: (err, updatedTodo, context) => {
queryClient.setQueryData(['todos'], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
Pagination
function PaginatedUsers() {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['users', page],
queryFn: () => fetch(`/api/users?page=${page}`).then(r => r.json()),
keepPreviousData: true
});
return (
<div>
{isLoading ? <Spinner /> : data.users.map(u => <UserCard key={u.id} user={u} />)}
<button onClick={() => setPage(p => p - 1)} disabled={page === 1}>Prev</button>
<button onClick={() => setPage(p => p + 1)} disabled={!data?.hasMore}>Next</button>
</div>
);
}
DevTools
npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
TanStack Query eliminates most manual loading/error state management for API data.