React Query vs. SWR: Comparing React Server State Management Hooks
Managing server state in modern React applications requires a fundamental shift away from traditional client-state containers like Redux or Context API. Both TanStack Query (React Query) and SWR (Stale-While-Revalidate) solve the challenges of async data fetching, automatic caching, deduplication, and background revalidation. However, they differ significantly in scope, feature density, and architectural philosophy.
In this guide, we will break down the core paradigms of both libraries, compare code patterns for fetching and mutations, evaluate performance trade-offs, and establish clear criteria for choosing the right solution.
1. Core Philosophies: Lightweight vs. Feature-Complete
To choose between these two libraries, it helps to understand the engineering philosophy behind each:
-
SWR (Created by Vercel): Built around the HTTP
stale-while-revalidatecache invalidation strategy. It prioritizes extreme simplicity, minimal bundle footprint (~4KB), and seamless integration with Next.js and Vercel infrastructure. - React Query (TanStack Query): Designed as a comprehensive, framework-agnostic asynchronous state manager. It provides advanced caching lifecycle controls, built-in mutation states, infinite scrolling utilities, offline support, and powerful DevTools out of the box (~13KB).
2. Basic Data Fetching Comparison
Both libraries utilize custom hooks that accept a unique cache key and an asynchronous fetching function.
A. Data Fetching with SWR
SWR requires configuring a global or local fetcher function (such as fetch or axios) to handle execution:
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR(`/api/users/${userId}`, fetcher);
if (isLoading) return <p>Loading user details...</p>;
if (error) return <p>Failed to load user profile.</p>;
return (
<div>
<h2>{data.name}</h2>
<p>Email: {data.email}</p>
</div>
);
}
B. Data Fetching with React Query
React Query uses array-based query keys for precise cache targeting and structured object returns:
import { useQuery } from '@tanstack/react-query';
async function fetchUser(userId: string) {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Network error');
return res.json();
}
export function UserProfile({ userId }: { userId: string }) {
const { data, isPending, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isPending) return <p>Loading user details...</p>;
if (isError) return <p>Error: {(error as Error).message}</p>;
return (
<div>
<h2>{data.name}</h2>
<p>Email: {data.email}</p>
</div>
);
}
3. Mutations and Cache Invalidation
Handling POST, PUT, and DELETE updates is where the structural differences between SWR and React Query become most evident.
Mutations in React Query (`useMutation`)
React Query provides a dedicated useMutation hook that manages pending, success, and error states out of the box, integrated directly with cache invalidation APIs:
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function UpdateUserForm({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newName: string) => {
return fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify({ name: newName }),
});
},
onSuccess: () => {
// Invalidate cache key to trigger automatic re-fetch
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
return (
<button onClick={() => mutation.mutate('New Name')}>
{mutation.isPending ? 'Saving...' : 'Update Name'}
</button>
);
}
Mutations in SWR (`useSWRMutation` / `mutate`)
SWR uses a programmatic mutate function or the extension hook useSWRMutation to revalidate keys manually:
import useSWRMutation from 'swr/mutation';
async function updateUser(url: string, { arg }: { arg: { name: string } }) {
return fetch(url, {
method: 'PATCH',
body: JSON.stringify(arg),
}).then((res) => res.json());
}
export function UpdateUserForm({ userId }: { userId: string }) {
const { trigger, isMutating } = useSWRMutation(`/api/users/${userId}`, updateUser);
return (
<button onClick={() => trigger({ name: 'New Name' })}>
{isMutating ? 'Saving...' : 'Update Name'}
</button>
);
}
4. Architectural Comparison Matrix
| Feature / Capability | React Query (TanStack) | SWR |
|---|---|---|
| Bundle Size | ~13 KB minified + gzipped | ~4 KB minified + gzipped |
| Cache Key Structure | Hierarchical Arrays (e.g. ['posts', id]) |
Strings, Functions, or Arrays |
| DevTools | Dedicated, feature-rich official package | Community-maintained browser extensions |
| Offline Support | Built-in persistors and offline queueing | Requires custom cache provider configuration |
| Infinite Loading | useInfiniteQuery with bidirectional page hooks |
useSWRInfinite module |
| Optimistic UI Updates | Native lifecycle hooks (onMutate, onError) |
Supported via populateCache & optimisticData |
5. How to Choose: Decision Framework
Select the tool that best aligns with your team's architecture and app requirements:
- Choose SWR if: You are building light-to-medium Next.js applications, prioritize ultra-small bundle sizes, or want a minimal, zero-boilerplate data fetching utility that just works out of the box.
- Choose React Query if: You are constructing large enterprise single-page apps (SPAs), complex dashboards, apps requiring robust offline capabilities, multi-step mutations with optimistic UI rollbacks, or deep inspection using professional DevTools.
Conclusion
Both SWR and React Query are modern, production-tested solutions for server state management in React. While SWR provides a sleek, lightweight experience tailored for Vercel and simple API revalidation, React Query provides an end-to-end asynchronous architecture that scales seamlessly to handle complex enterprise requirements.
Happy Coding! 🚀
Comments
Post a Comment