SWR Complete Guide | React Data Fetching by Vercel
이 글의 핵심
SWR (stale-while-revalidate) is a React Hooks library for data fetching by Vercel. It provides caching, revalidation, focus tracking, and real-time updates out of the box.
Introduction
SWR is a React Hooks library for data fetching created by Vercel (the team behind Next.js). The name comes from stale-while-revalidate, an HTTP caching strategy that returns cached data immediately while fetching fresh data in the background.
The Problem
Traditional data fetching:
useEffect(() => {
setLoading(true);
fetch('/api/user')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);
Problems:
- Manual loading state
- No caching
- No revalidation
- Boilerplate code
The Solution
With SWR:
const { data, error, isLoading } = useSWR('/api/user', fetcher);
1. Installation
npm install swr
2. Basic Usage
Simple Fetching
import useSWR from 'swr';
// Fetcher function
const fetcher = (url: string) => fetch(url).then(res => res.json());
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load</div>;
return <div>Hello, {data.name}!</div>;
}
With TypeScript
interface User {
id: number;
name: string;
email: string;
}
function Profile() {
const { data, error, isLoading } = useSWR<User>(
'/api/user',
fetcher
);
return <div>{data?.name}</div>;
}
3. Global Configuration
// app/layout.tsx (Next.js)
import { SWRConfig } from 'swr';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
fetcher: (url: string) => fetch(url).then(res => res.json()),
revalidateOnFocus: false,
revalidateOnReconnect: true,
}}
>
{children}
</SWRConfig>
);
}
Now you don’t need to pass fetcher every time:
const { data } = useSWR('/api/user'); // Fetcher is global
4. Mutations
Basic Mutation
import useSWR, { mutate } from 'swr';
function UpdateProfile() {
const { data } = useSWR('/api/user', fetcher);
async function updateUser(newName: string) {
// Update API
await fetch('/api/user', {
method: 'PATCH',
body: JSON.stringify({ name: newName }),
});
// Revalidate
mutate('/api/user');
}
return <button onClick={() => updateUser('New Name')}>Update</button>;
}
Optimistic Updates
import useSWR, { mutate } from 'swr';
function TodoList() {
const { data: todos } = useSWR<Todo[]>('/api/todos', fetcher);
async function addTodo(title: string) {
const newTodo = { id: Date.now(), title, completed: false };
// Optimistic update
mutate(
'/api/todos',
async (currentTodos) => {
// Update UI immediately
const updatedTodos = [...(currentTodos || []), newTodo];
// Send request
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
// Return optimistic data
return updatedTodos;
},
{
optimisticData: [...(todos || []), newTodo],
rollbackOnError: true,
}
);
}
return (
<div>
{todos?.map(todo => <div key={todo.id}>{todo.title}</div>)}
</div>
);
}
Bound Mutate
import useSWR from 'swr';
function Profile() {
const { data, mutate } = useSWR('/api/user', fetcher);
async function updateUser() {
// Update with bound mutate (no need to pass key)
await mutate(async (user) => {
await fetch('/api/user', { method: 'PATCH', body: {...} });
return { ...user, name: 'Updated' };
});
}
return <div>{data?.name}</div>;
}
5. Conditional Fetching
function UserProfile({ userId }: { userId?: number }) {
// Only fetch if userId exists
const { data } = useSWR(
userId ? `/api/users/${userId}` : null,
fetcher
);
return <div>{data?.name}</div>;
}
6. Dependent Requests
function UserPosts({ userId }: { userId: number }) {
// First request
const { data: user } = useSWR(`/api/users/${userId}`, fetcher);
// Second request depends on first
const { data: posts } = useSWR(
user ? `/api/posts?userId=${user.id}` : null,
fetcher
);
return <div>{posts?.length} posts</div>;
}
7. Pagination
function UserList() {
const [page, setPage] = useState(1);
const { data, error, isLoading } = useSWR(
`/api/users?page=${page}&limit=20`,
fetcher
);
return (
<div>
{data?.users.map((user: any) => (
<div key={user.id}>{user.name}</div>
))}
<button onClick={() => setPage(page - 1)} disabled={page === 1}>
Previous
</button>
<button onClick={() => setPage(page + 1)}>Next</button>
</div>
);
}
8. Infinite Loading
import useSWRInfinite from 'swr/infinite';
function InfiniteList() {
const getKey = (pageIndex: number, previousPageData: any) => {
// Reached the end
if (previousPageData && !previousPageData.hasMore) return null;
// First page
return `/api/users?page=${pageIndex + 1}&limit=20`;
};
const { data, size, setSize, isLoading } = useSWRInfinite(
getKey,
fetcher
);
const users = data ? data.flatMap(page => page.users) : [];
const hasMore = data?.[data.length - 1]?.hasMore;
return (
<div>
{users.map((user) => (
<div key={user.id}>{user.name}</div>
))}
{hasMore && (
<button onClick={() => setSize(size + 1)}>Load More</button>
)}
</div>
);
}
9. Real-time Updates
Auto Revalidation
const { data } = useSWR('/api/data', fetcher, {
refreshInterval: 3000, // Refresh every 3 seconds
refreshWhenHidden: false, // Pause when tab hidden
refreshWhenOffline: false, // Pause when offline
});
Manual Revalidation
import { useSWRConfig } from 'swr';
function RefreshButton() {
const { mutate } = useSWRConfig();
return (
<button onClick={() => mutate('/api/user')}>
Refresh
</button>
);
}
10. Error Handling
Retry on Error
const { data, error } = useSWR('/api/user', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Never retry on 404
if (error.status === 404) return;
// Only retry 3 times
if (retryCount >= 3) return;
// Retry after 5 seconds
setTimeout(() => revalidate({ retryCount }), 5000);
},
});
Error Handling
const { data, error } = useSWR('/api/user', fetcher, {
onError: (error, key) => {
console.error('SWR error:', error);
toast.error('Failed to fetch data');
},
onSuccess: (data, key, config) => {
console.log('Data loaded:', data);
},
});
11. Prefetching
import { mutate } from 'swr';
function UserList({ users }: { users: User[] }) {
const prefetch = (userId: number) => {
// Prefetch user data
mutate(
`/api/users/${userId}`,
fetch(`/api/users/${userId}`).then(res => res.json())
);
};
return (
<ul>
{users.map((user) => (
<li key={user.id} onMouseEnter={() => prefetch(user.id)}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}
12. Middleware
import useSWR from 'swr';
// Logging middleware
function logger(useSWRNext: any) {
return (key: any, fetcher: any, config: any) => {
const swr = useSWRNext(key, fetcher, config);
useEffect(() => {
console.log('SWR Request:', key);
}, [key]);
return swr;
};
}
// Use middleware
const { data } = useSWR('/api/user', fetcher, { use: [logger] });
13. Best Practices
1. Create Custom Hooks
// hooks/useUser.ts
export function useUser(id: number) {
return useSWR<User>(
id ? `/api/users/${id}` : null,
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 2000,
}
);
}
// Usage
const { data: user } = useUser(123);
2. Handle Loading States
function Component() {
const { data, error, isLoading, isValidating } = useSWR(key, fetcher);
// First load
if (isLoading) return <Skeleton />;
// Error state
if (error) return <ErrorMessage error={error} />;
// Background revalidation indicator
return (
<div>
{isValidating && <RefreshIcon className="animate-spin" />}
{data && <DataDisplay data={data} />}
</div>
);
}
3. Cache Management
import { useSWRConfig } from 'swr';
function CacheManager() {
const { cache, mutate } = useSWRConfig();
const clearCache = () => {
// Clear specific key
mutate('/api/user', undefined, { revalidate: false });
// Clear all cache
if (cache instanceof Map) {
cache.clear();
}
};
return <button onClick={clearCache}>Clear Cache</button>;
}
14. Next.js Integration
API Routes
// app/api/users/route.ts (Next.js 13+)
export async function GET() {
const users = await prisma.user.findMany();
return Response.json({ users });
}
Client Component
'use client';
import useSWR from 'swr';
export function UserList() {
const { data } = useSWR('/api/users', fetcher);
return (
<ul>
{data?.users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
15. Performance Tips
Deduplication
// Multiple components can use the same key
// SWR makes only ONE request
function ComponentA() {
const { data } = useSWR('/api/user', fetcher);
}
function ComponentB() {
const { data } = useSWR('/api/user', fetcher); // Uses same cache
}
Focus Revalidation
const { data } = useSWR('/api/data', fetcher, {
// Revalidate when tab/window gains focus
revalidateOnFocus: true,
// Minimum interval between revalidations
focusThrottleInterval: 5000, // 5 seconds
});
Summary
SWR simplifies React data fetching:
- Automatic caching with stale-while-revalidate
- Smart revalidation on focus, reconnect, interval
- Optimistic updates for instant UI feedback
- Real-time support with polling and WebSocket
- Lightweight - only 4KB gzipped
Key Takeaways:
- Returns cached data first, fetches fresh data in background
- Use
mutatefor optimistic updates - Configure globally with
SWRConfig - Create custom hooks for reusability
- Perfect for Next.js and Vercel deployments
Next Steps:
- Compare with TanStack Query
- Learn Next.js 15
- Build real-time apps with React 18
Resources: