9/22/2025
React Query vs SWR — server state management in 2025
TL;DR: React Query (TanStack Query) = full toolkit (mutations, devtools, optimistic updates). SWR = simpler, smaller bundle. For most projects React Query wins on functionality.
If you’re still writing useState + useEffect for fetching data from an API, you’re spending time solving problems that have already been solved. Server state management is a separate discipline from local UI state — data you fetch from the backend has different requirements than form state.
The problem they solve
Classic implementation:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
Problems with this code: no cache (every component mount = a new request), no deduplication (10 components on the page = 10 identical requests), race condition when a component unmounts between request and response (setState on unmounted component), no automatic refresh when returning to a tab, no retry on network error.
React Query and SWR solve all of these problems out of the box.
SWR — 5-minute quickstart
SWR (stale-while-revalidate) from Vercel:
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
function UserProfile({ userId }) {
const { data, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data.name}</div>;
}
SWR automatically: deduplicates requests with the same key, revalidates on window focus (returned to tab = data refreshed), handles race conditions, retries on network errors (exponential backoff). The entire SWR bundle is ~4kb gzipped.
React Query — more power
React Query (TanStack Query) has a more feature-rich API:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function TaskList() {
const queryClient = useQueryClient();
const { data: tasks } = useQuery({
queryKey: ['tasks'],
queryFn: () => fetch('/api/tasks').then(r => r.json()),
staleTime: 1000 * 60 * 5, // data is "fresh" for 5 minutes
});
const addTask = useMutation({
mutationFn: (newTask) => fetch('/api/tasks', {
method: 'POST',
body: JSON.stringify(newTask),
}),
onSuccess: () => {
// After adding a task — refresh the list
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
return (
<>
{tasks?.map(t => <Task key={t.id} task={t} />)}
<button onClick={() => addTask.mutate({ title: 'New task' })}>
{addTask.isPending ? 'Adding...' : 'Add task'}
</button>
</>
);
}
invalidateQueries after a mutation is a pattern that in SWR you’d need to implement manually via mutate(). In React Query it’s built-in and works for all components subscribing to that query key.
Devtools
React Query DevTools is an argument that can by itself determine the choice in a team project:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// In root component
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
In the devtools panel you see in real time: all active queries with their state (fresh/stale/fetching/error), time of last fetch, observer count (how many components are subscribed), cache contents, request timing. When debugging “why isn’t the data refreshing” this is a killer feature.
SWR has no official devtools.
When SWR
SWR is the right choice when: the project is small and simple (mainly fetching, few mutations), you’re using Next.js and want natural integration with the Vercel ecosystem, bundle size is critical (SWR 4kb vs React Query ~13kb gzipped), the team prefers a minimal API without ceremony.
When React Query
React Query wins when: you have complex mutations with optimistic updates (update UI immediately, roll back if server returns error), dependent queries (useQuery({ enabled: !!userId }) — second query only runs when you have userId from the first), you need fine-grained control over staleness and background refetching, a large team that will benefit from devtools during debugging.
Summary
For new React projects — start with React Query. The rich API and DevTools will pay for themselves the first time you encounter a complex mutation scenario or dependent queries. Leave SWR for Next.js projects where simplicity and a small bundle are the priority, or when the only use case is simple read-only data fetching.