TanStack Query
TanStack Query (formerly React Query): server state for React. Caching, refetching, mutations, and the patterns that replace useEffect for data.
React — TanStack Query
EXAMPLE
// Install: npm install @tanstack/react-query
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// ===== Setup =====
const qc = new QueryClient({
defaultOptions: { queries: { staleTime: 60_000, retry: 1 } },
});
function App() {
return <QueryClientProvider client={qc}><Routes /></QueryClientProvider>;
}
// ===== Query =====
function UserList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const r = await fetch('/api/users');
if (!r.ok) throw new Error('failed');
return r.json();
},
staleTime: 60_000,
});
if (isLoading) return <p>Loading</p>;
if (error) return <p>Error</p>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ===== Query keys =====
useQuery({ queryKey: ['user', userId] }); // per-id cache
useQuery({ queryKey: ['posts', { tag, page }] }); // structured key
// ===== Mutation =====
function CreatePost() {
const qc = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: (newPost) => fetch('/api/posts', { method: 'POST', body: JSON.stringify(newPost) }).then(r => r.json()),
onSuccess: () => qc.invalidateQueries({ queryKey: ['posts'] }),
});
return <button disabled={isPending} onClick={() => mutate({ title: 'hi' })}>Create</button>;
}
// ===== Optimistic update =====
const { mutate } = useMutation({
mutationFn: updatePost,
onMutate: async (newPost) => {
await qc.cancelQueries({ queryKey: ['posts'] });
const prev = qc.getQueryData(['posts']);
qc.setQueryData(['posts'], (old) => old.map(p => p.id === newPost.id ? newPost : p));
return { prev };
},
onError: (err, newPost, ctx) => qc.setQueryData(['posts'], ctx.prev),
onSettled: () => qc.invalidateQueries({ queryKey: ['posts'] }),
});
// ===== Infinite query =====
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetch(\`/api/posts?cursor=${pageParam}\`).then(r => r.json()),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: 0,
});
// ===== Devtools =====
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<ReactQueryDevtools initialIsOpen={false} />
// ===== Patterns =====
// - Stop writing useEffect for fetches
// - Use queryKey arrays for cache scoping
// - Invalidate after mutations to refetch dependents
// - staleTime > 0 to dedupe identical queries
// ===== Pitfalls =====
// - Different queryKey per render -> new cache entries each render
// - Mutation onSuccess without invalidate -> stale UI
// - Using as global state replacement -> use Zustand / Context for client state
Why it matters
TanStack Query is the standard for server state in React. Replace useEffect-fetches with useQuery, cache by queryKey, mutate + invalidate. Add optimistic updates and infinite queries when needed. The mental shift: server state is its own thing, not generic state.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import { useQuery } from '@tanstack/react-query';
const { data, isLoading } = useQuery({
queryKey: ['users'], queryFn: () => fetch('/api/users').then(r => r.json()),
});
Try it Yourself »
Discussion
Loading…