What is SWR
SWR (stale-while-revalidate) is a data fetching library for React Hooks developed by Vercel. Based on the HTTP cache invalidation strategy, it achieves fast and reactive data fetching.
sequenceDiagram
participant C as Client
participant S as SWR Cache
participant A as API Server
Note over C,A: 1. Initial Request
C->>S: Request data
S->>A: Fetch (cache empty)
A-->>S: Response
S-->>C: Return data
Note over C,A: 2. Subsequent Request (with cache)
C->>S: Request data
S-->>C: Return stale data immediately
S->>A: Background revalidation
A-->>S: Fresh data
S-->>C: Update with new data
Basic Usage
Installation and Setup
npm install swr
export const fetcher = async <T>(url: string): Promise<T> => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error('API request failed');
throw error;
}
return res.json();
};
export const authFetcher = async <T>(url: string): Promise<T> => {
const token = localStorage.getItem('token');
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) {
if (res.status === 401) {
throw new Error('Authentication error');
}
throw new Error('API request failed');
}
return res.json();
};
useSWR Hook
import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
interface User {
id: string;
name: string;
email: string;
avatar: string;
}
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading, isValidating, mutate } = useSWR<User>(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>An error occurred</div>;
if (!data) return null;
return (
<div>
<img src={data.avatar} alt={data.name} />
<h1>{data.name}</h1>
<p>{data.email}</p>
{isValidating && <span>Updating...</span>}
<button onClick={() => mutate()}>Refetch</button>
</div>
);
}
Global Configuration
import { SWRConfig } from 'swr';
import { fetcher } from '@/lib/fetcher';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
fetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0,
shouldRetryOnError: true,
errorRetryCount: 3,
errorRetryInterval: 5000,
dedupingInterval: 2000,
onError: (error, key) => {
console.error(`SWR Error [${key}]:`, error);
},
onSuccess: (data, key) => {
console.log(`SWR Success [${key}]:`, data);
},
}}
>
{children}
</SWRConfig>
);
}
SWR Configuration Options
| Category | Option | Default | Description |
|---|
| Revalidation Timing | revalidateOnFocus | true | On focus |
| revalidateOnReconnect | true | On reconnect |
| refreshInterval | 0 | Periodic update (ms) |
| refreshWhenHidden | false | Update when hidden |
| refreshWhenOffline | false | Update when offline |
| Performance | dedupingInterval | 2000 | Dedup interval (ms) |
| focusThrottleInterval | 5000 | Focus throttle |
| loadingTimeout | 3000 | Loading threshold |
| Error Handling | shouldRetryOnError | true | Retry on error |
| errorRetryCount | 3 | Retry count |
| errorRetryInterval | 5000 | Retry interval |
Conditional Fetching
function Dashboard() {
const { user } = useAuth();
const { data: profile } = useSWR(
user ? `/api/users/${user.id}/profile` : null,
fetcher
);
const { data: posts } = useSWR(
() => (user ? `/api/users/${user.id}/posts` : null),
fetcher
);
return (
<div>
{profile && <ProfileCard profile={profile} />}
{posts && <PostList posts={posts} />}
</div>
);
}
function UserPosts({ userId }: { userId: string }) {
const { data: user } = useSWR<User>(`/api/users/${userId}`, fetcher);
const { data: posts } = useSWR<Post[]>(
user ? `/api/users/${user.id}/posts` : null,
fetcher
);
return (
<div>
<h1>{user?.name}'s posts</h1>
{posts?.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}
Mutations
Optimistic Updates
import useSWR, { useSWRConfig } from 'swr';
interface Todo {
id: string;
title: string;
completed: boolean;
}
function TodoList() {
const { data: todos, mutate } = useSWR<Todo[]>('/api/todos', fetcher);
const { mutate: globalMutate } = useSWRConfig();
const toggleTodo = async (todo: Todo) => {
const updatedTodo = { ...todo, completed: !todo.completed };
mutate(
todos?.map(t => t.id === todo.id ? updatedTodo : t),
false
);
try {
await fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: updatedTodo.completed }),
});
mutate();
} catch (error) {
mutate(todos, false);
alert('Update failed');
}
};
const addTodo = async (title: string) => {
const tempId = `temp-${Date.now()}`;
const newTodo: Todo = { id: tempId, title, completed: false };
mutate([...(todos || []), newTodo], false);
try {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
const createdTodo = await res.json();
mutate(
todos?.map(t => t.id === tempId ? createdTodo : t),
false
);
} catch (error) {
mutate(todos?.filter(t => t.id !== tempId), false);
}
};
return (
<ul>
{todos?.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo)}>
<input type="checkbox" checked={todo.completed} readOnly />
{todo.title}
</li>
))}
</ul>
);
}
Explicit Mutations with useSWRMutation
import useSWRMutation from 'swr/mutation';
interface CreatePostInput {
title: string;
content: string;
}
async function createPost(url: string, { arg }: { arg: CreatePostInput }) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(arg),
});
return res.json();
}
function CreatePostForm() {
const { trigger, isMutating, error } = useSWRMutation(
'/api/posts',
createPost
);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
try {
const result = await trigger({
title: formData.get('title') as string,
content: formData.get('content') as string,
});
console.log('Post created:', result);
} catch (error) {
console.error('Post failed:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit" disabled={isMutating}>
{isMutating ? 'Posting...' : 'Post'}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}
import useSWRInfinite from 'swr/infinite';
interface Post {
id: string;
title: string;
content: string;
}
interface PostsResponse {
posts: Post[];
nextCursor: string | null;
}
const PAGE_SIZE = 10;
function InfinitePostList() {
const getKey = (pageIndex: number, previousPageData: PostsResponse | null) => {
if (previousPageData && !previousPageData.nextCursor) return null;
if (pageIndex === 0) return `/api/posts?limit=${PAGE_SIZE}`;
return `/api/posts?cursor=${previousPageData?.nextCursor}&limit=${PAGE_SIZE}`;
};
const {
data,
error,
size,
setSize,
isLoading,
isValidating,
} = useSWRInfinite<PostsResponse>(getKey, fetcher);
const posts = data?.flatMap(page => page.posts) ?? [];
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
const isEmpty = data?.[0]?.posts.length === 0;
const isReachingEnd = isEmpty || (data && !data[data.length - 1]?.nextCursor);
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
{isLoadingMore && <div>Loading...</div>}
{!isReachingEnd && (
<button
onClick={() => setSize(size + 1)}
disabled={isLoadingMore}
>
Load more
</button>
)}
{isReachingEnd && !isEmpty && (
<p>All posts displayed</p>
)}
</div>
);
}
flowchart TB
subgraph Page0["pageIndex: 0"]
P0["/api/posts?limit=10"]
R0["{ posts: [...], nextCursor: 'abc' }"]
P0 --> R0
end
subgraph Page1["pageIndex: 1"]
P1["/api/posts?cursor=abc&limit=10"]
R1["{ posts: [...], nextCursor: 'def' }"]
P1 --> R1
end
subgraph Page2["pageIndex: 2"]
P2["/api/posts?cursor=def&limit=10"]
R2["{ posts: [...], nextCursor: null }"]
P2 --> R2
end
End["nextCursor: null → return null (finished)"]
Page0 --> Page1
Page1 --> Page2
Page2 --> End
Custom Hooks
import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
interface Post {
id: string;
title: string;
content: string;
author: { id: string; name: string };
createdAt: string;
}
interface UsePostsOptions {
limit?: number;
tag?: string;
}
export function usePosts(options: UsePostsOptions = {}) {
const { limit = 10, tag } = options;
const params = new URLSearchParams();
params.set('limit', String(limit));
if (tag) params.set('tag', tag);
return useSWR<Post[]>(
`/api/posts?${params.toString()}`,
fetcher,
{
revalidateOnFocus: false,
dedupingInterval: 10000,
}
);
}
export function usePost(id: string | null) {
return useSWR<Post>(
id ? `/api/posts/${id}` : null,
fetcher
);
}
export function useUser() {
const { data, error, isLoading, mutate } = useSWR<User>(
'/api/auth/me',
fetcher,
{
revalidateOnFocus: true,
errorRetryCount: 0,
}
);
return {
user: data,
isLoading,
isLoggedIn: !!data && !error,
isError: error,
mutate,
};
}
Prefetching
import { preload } from 'swr';
import { fetcher } from '@/lib/fetcher';
function PostLink({ postId }: { postId: string }) {
const handleMouseEnter = () => {
preload(`/api/posts/${postId}`, fetcher);
};
return (
<Link
href={`/posts/${postId}`}
onMouseEnter={handleMouseEnter}
>
View details
</Link>
);
}
function PostsPage() {
useEffect(() => {
preload('/api/posts/popular', fetcher);
}, []);
return <div>...</div>;
}
export async function getStaticProps() {
const posts = await fetcher('/api/posts');
return {
props: {
fallback: {
'/api/posts': posts,
},
},
};
}
function Page({ fallback }) {
return (
<SWRConfig value={{ fallback }}>
<PostList />
</SWRConfig>
);
}
Error Handling
import useSWR from 'swr';
class APIError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.status = status;
}
}
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) {
const error = new APIError(
'API request failed',
res.status
);
throw error;
}
return res.json();
};
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR<User, APIError>(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <LoadingSpinner />;
if (error) {
switch (error.status) {
case 404:
return <NotFound message="User not found" />;
case 401:
return <Redirect to="/login" />;
case 500:
return <ErrorPage message="Server error occurred" />;
default:
return <ErrorPage message={error.message} />;
}
}
return <ProfileCard user={data!} />;
}
function DataFetchingErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
onError: (error, key) => {
reportError(error, { key });
},
}}
>
{children}
</SWRConfig>
);
}
SWR vs React Query Comparison
| Feature | SWR | React Query |
|---|
| Bundle Size | ~4KB | ~13KB |
| Learning Curve | Low | Medium |
| DevTools | None | Excellent |
| Mutations | Simple | Advanced |
| Infinite Scroll | useSWRInfinite | useInfiniteQuery |
| SSR Support | Good | Good |
| Cache Control | Simple | Detailed |
Reference Links
← Back to list