TanStack
Overview
If you aren't using a meta-framework with Server Components (like Next.js) and are building a standard SPA (like Vite), raw `useEffect` data fetching is considered legacy. TanStack Query (React Query) is the industry standard for managing 'Server State'. It provides out-of-the-box caching, deduplication, background refetching, pagination, and optimistic updates.
Syntax
If you render `TodoList` in 5 different places on the screen, React Query is smart enough to only make ONE network request and share the cached data to all 5 components instantly.
import { useQuery } from '@tanstack/react-query';
function TodoList() {
// useQuery handles ALL the messy state for you
const { data, isLoading, isError, error } = useQuery({
queryKey: ['todos'], // The unique cache key for this data
queryFn: () => fetch('/api/todos').then(res => res.json())
});
if (isLoading) return <span>Loading...</span>;
if (isError) return <span>Error: {error.message}</span>;
return (
<ul>
{data.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
);
}`useMutation` is used for POST/PUT/DELETE requests. Invalidation is the killer feature: telling React Query that the cache is stale triggers automatic UI updates everywhere.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) => fetch('/api/todos', { method: 'POST', body: newTodo }),
onSuccess: () => {
// Invalidate the cache!
// This forces the 'todos' query above to instantly refetch in the background
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button onClick={() => mutation.mutate({ title: 'Do chores' })}>
Add Todo
</button>
);
}Common Pitfalls
- Forgetting to wrap your app in the `<QueryClientProvider>`. The hooks will crash without it.
- Using objects or complex arrays directly inside `queryKey` without understanding that React Query serializes them deterministically.
Interview Tips
- Understand the difference between Client State (managed by useState/Zustand) and Server State (managed by React Query). Server State is async, shared, and out of your control. Client State is synchronous, local, and fully in your control.
Real-World Example
Optimistic Updates. When a user clicks 'Like', React Query can instantly update the UI cache to show a filled heart, and silently revert it if the network request fails a second later.
const likeMutation = useMutation({
mutationFn: likePost,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const previousPosts = queryClient.getQueryData(['posts']);
// Optimistically update the cache
queryClient.setQueryData(['posts'], old => old.map(p => p.id === postId ? { ...p, liked: true } : p));
return { previousPosts };
},
onError: (err, postId, context) => {
// Revert if it fails
queryClient.setQueryData(['posts'], context.previousPosts);
}
});