SWR 是由 Vercel 创建的 React Hooks 数据获取库。SWR 的名称来自 stale-while-revalidate,这是一种 HTTP 缓存失效策略:首先返回缓存(陈旧)数据,然后发送获取请求(重新验证),最终返回最新结果。SWR 为 React 应用提供了简单而强大的 API,内置支持分页、预取、SSR 和实时更新。
SWR 是 Vercel 出品的轻量级 React 数据获取库,实现了 stale-while-revalidate 缓存策略。它提供自动缓存、请求去重、焦点重新验证、轮询、乐观更新、useSWRInfinite 分页、Next.js SSR/SSG 支持,以及 TypeScript 优先设计。压缩后不到 4KB,为现代 React 应用提供快速可靠的数据层。
- SWR 立即返回缓存数据并在后台重新验证,提供即时响应体验和始终新鲜的数据。
- 内置请求去重意味着多个使用相同 key 的组件自动共享单个网络请求。
- 焦点重新验证、轮询和重连重新验证无需手动干预即可保持数据新鲜。
- useSWRMutation 将远程变更与数据获取分离,支持乐观更新和错误回滚。
- useSWRInfinite 为无限滚动和分页数据加载提供一流 API。
- SWR 与 Next.js 无缝集成,支持服务端渲染和静态生成以及客户端重新验证。
什么是 SWR,Stale-While-Revalidate 如何工作?
SWR 是一种策略:请求时立即返回缓存(陈旧)数据,然后在后台发送重新验证请求,最后用新响应更新缓存。用户立即从缓存看到数据,同时库静默确保数据保持最新。
useSWR hook 是核心 API。你给它一个 key(通常是 URL)和一个 fetcher 函数。SWR 自动处理缓存、去重、重新验证、错误重试和焦点跟踪。当多个组件使用相同 key 时,它们共享同一个缓存条目,只发送一个网络请求。
import useSWR from 'swr';
// Define a fetcher function
const fetcher = (url) => fetch(url).then((res) => res.json());
function UserProfile({ userId }) {
const { data, error, isLoading } = useSWR(
'/api/users/' + userId,
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading user</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}SWR 与 React Query(TanStack Query)对比
SWR 和 TanStack Query(前身为 React Query)是 React 中最流行的两个数据获取库。两者解决类似问题,但在复杂度和功能范围上有所不同。
| 特性 | SWR | TanStack Query |
|---|---|---|
| 包大小 | ~4KB gzipped | ~13KB gzipped |
| 缓存 | Stale-while-revalidate | Stale-while-revalidate + GC |
| 开发工具 | 社区版(SWRDevTools) | 官方内置 |
| 变更操作 | useSWRMutation + mutate | useMutation(完整生命周期) |
| 分页 | useSWRInfinite | useInfiniteQuery + 分页 |
| SSR 支持 | Next.js 一流支持 | 框架无关 SSR |
| TypeScript | 优秀(内置) | 优秀(内置) |
| 垃圾回收 | 手动(cache provider) | 自动(可配置) |
| 离线支持 | 基础(通过配置) | 高级(变更队列) |
| 学习曲线 | 低(简单 API) | 中等(概念较多) |
基本用法:useSWR Hook
useSWR hook 接受一个 key 和一个 fetcher 函数。key 是标识数据的唯一字符串(通常是 API URL)。fetcher 是实际执行数据获取的函数。SWR 返回 data、error、isLoading 和 isValidating 状态。
fetcher 函数接收 key 作为参数并返回 Promise。你可以使用 fetch、axios、graphql-request 或任何异步函数作为 fetcher。
import useSWR from 'swr';
// Fetcher with error handling
const fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error('Failed to fetch');
error.info = await res.json();
error.status = res.status;
throw error;
}
return res.json();
};
function Dashboard() {
// data: response data (undefined if not yet loaded)
// error: error thrown by fetcher (undefined if no error)
// isLoading: true on first fetch with no cached data
// isValidating: true whenever a request is in flight
const { data, error, isLoading, isValidating, mutate } = useSWR(
'/api/dashboard',
fetcher,
{
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 30000, // poll every 30 seconds
}
);
if (isLoading) return <div>Loading dashboard...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Dashboard</h1>
{isValidating && <span>Refreshing...</span>}
<p>Total users: {data.userCount}</p>
<p>Revenue: {data.revenue}</p>
<button onClick={() => mutate()}>Refresh</button>
</div>
);
}全局配置:SWRConfig
SWRConfig 提供上下文来设置应用中所有 SWR hook 的默认选项。避免在每个 useSWR 调用中重复相同的 fetcher、错误重试逻辑或重新验证间隔。
可以嵌套 SWRConfig 来覆盖特定部分的选项。子配置与父配置合并,只需指定要更改的选项。
import { SWRConfig } from 'swr';
const globalFetcher = (url) => fetch(url).then((r) => r.json());
function App() {
return (
<SWRConfig
value={{
fetcher: globalFetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 2000,
errorRetryCount: 3,
onError: (error, key) => {
if (error.status === 403) {
// Handle auth errors globally
redirectToLogin();
}
},
}}
>
<Dashboard />
</SWRConfig>
);
}
// No fetcher needed — uses global config
function Dashboard() {
const { data } = useSWR('/api/dashboard');
return <div>{data?.title}</div>;
}条件获取
SWR 支持通过传递 null 或假值作为 key 进行条件获取。当 key 为 null 时,SWR 不会发起请求。适用于数据依赖于用户认证、功能开关或其他运行时条件的场景。
也可以传递函数作为 key。如果函数抛出错误或返回假值,SWR 会暂停请求。这是依赖获取的推荐模式。
// Pass null to disable fetching
function ProtectedData({ isLoggedIn }) {
const { data } = useSWR(
isLoggedIn ? '/api/protected' : null,
fetcher
);
if (!isLoggedIn) return <div>Please log in</div>;
return <div>{data?.message}</div>;
}
// Use a function key that throws when dependency is missing
function UserOrders({ userId }) {
const { data } = useSWR(
() => userId ? '/api/users/' + userId + '/orders' : null,
fetcher
);
return <div>{data?.orders.length} orders</div>;
}依赖请求
依赖请求是依赖前一个请求结果的获取。SWR 通过使用函数作为 key 来优雅处理。如果函数抛出异常(因为依赖尚不可用),SWR 会暂停直到依赖解析完成。
SWR 保证依赖请求按正确顺序发起,每一步都被正确缓存。当父数据更改时,依赖请求自动重新验证。
function UserDashboard() {
// First: fetch the current user
const { data: user } = useSWR('/api/me', fetcher);
// Second: fetch projects only when user is loaded
const { data: projects } = useSWR(
() => user ? '/api/users/' + user.id + '/projects' : null,
fetcher
);
// Third: fetch team only when user is loaded
const { data: team } = useSWR(
() => user ? '/api/teams/' + user.teamId : null,
fetcher
);
if (!user) return <div>Loading user...</div>;
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>{projects?.length ?? '...'} projects</p>
<p>Team: {team?.name ?? 'Loading...'}</p>
</div>
);
}变更:mutate 和 useSWRMutation
SWR 提供两种修改远程数据的方式。绑定的 mutate 函数(useSWR 返回)重新验证特定 key。全局 mutate 函数可以重新验证任何 key。对于带加载状态的专用变更,使用 useSWRMutation hook。
绑定 Mutate
useSWR 返回的 mutate 函数绑定到当前 key。调用它来重新验证数据或传递新数据直接更新缓存。
function TodoList() {
const { data: todos, mutate } = useSWR('/api/todos', fetcher);
const addTodo = async (text) => {
// Revalidate after mutation (refetch from server)
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
});
mutate(); // triggers revalidation
};
// Or update cache directly without revalidation
const toggleTodo = async (id) => {
await fetch('/api/todos/' + id + '/toggle', { method: 'PATCH' });
mutate(
todos.map((t) => t.id === id ? { ...t, done: !t.done } : t),
false // set to false to skip revalidation
);
};
return todos?.map((todo) => (
<div key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.text}
</div>
));
}全局 Mutate
useSWRConfig 中的全局 mutate 函数可以从应用任何位置变更任何 key。适用于变更后更新相关缓存。
import { useSWRConfig } from 'swr';
function CreateProject() {
const { mutate } = useSWRConfig();
const handleCreate = async (formData) => {
await fetch('/api/projects', {
method: 'POST',
body: JSON.stringify(formData),
});
// Revalidate the projects list
mutate('/api/projects');
// Revalidate all keys matching a filter
mutate(
(key) => typeof key === 'string' && key.startsWith('/api/projects'),
undefined,
{ revalidate: true }
);
};
return <button onClick={() => handleCreate({ name: 'New' })}>Create</button>;
}useSWRMutation Hook
useSWRMutation 专为远程变更(POST、PUT、DELETE)设计。与 useSWR 不同,它不会自动获取数据,而是提供一个手动调用的 trigger 函数。
import useSWRMutation from 'swr/mutation';
// Mutation fetcher receives (key, { arg })
async function createTodo(url, { arg }) {
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify(arg),
});
return res.json();
}
function AddTodo() {
const { trigger, isMutating, error } = useSWRMutation(
'/api/todos',
createTodo
);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const newTodo = await trigger({ text: 'Buy groceries' });
console.log('Created:', newTodo);
} catch (err) {
console.error('Failed:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<button disabled={isMutating}>
{isMutating ? 'Adding...' : 'Add Todo'}
</button>
{error && <span>Error: {error.message}</span>}
</form>
);
}乐观更新
乐观更新通过在服务器确认之前立即更新 UI 来改善感知性能。如果服务器请求失败,SWR 自动回滚到之前的数据。这种模式对于协作和实时应用中的响应式 UI 至关重要。
function LikeButton({ postId }) {
const { data, mutate } = useSWR('/api/posts/' + postId, fetcher);
const handleLike = async () => {
// Optimistic update: immediately show the new state
const optimisticData = { ...data, likes: data.likes + 1, liked: true };
await mutate(
async () => {
// This runs in the background
const res = await fetch('/api/posts/' + postId + '/like', {
method: 'POST',
});
return res.json();
},
{
optimisticData,
rollbackOnError: true, // revert if server fails
populateCache: true,
revalidate: false,
}
);
};
return (
<button onClick={handleLike}>
{data?.liked ? 'Liked' : 'Like'} ({data?.likes})
</button>
);
}分页:useSWRInfinite
useSWRInfinite 专为分页和无限滚动界面构建。它管理一个页面数组,每个页面有自己的缓存 key。你提供一个 getKey 函数为每个页面索引返回 key,SWR 自动处理加载、缓存和合并页面。
hook 返回 size(当前加载的页数)、setSize(加载更多的函数)和合并的 data 数组。还支持并行获取多个页面以加速加载。
import useSWRInfinite from 'swr/infinite';
const PAGE_SIZE = 10;
// getKey receives page index and previous page data
function getKey(pageIndex, previousPageData) {
// Reached the end
if (previousPageData && !previousPageData.length) return null;
// First page, no previous data
if (pageIndex === 0) return '/api/posts?limit=' + PAGE_SIZE;
// Use cursor from last item
return '/api/posts?cursor='
+ previousPageData[previousPageData.length - 1].id
+ '&limit=' + PAGE_SIZE;
}
function PostFeed() {
const { data, size, setSize, isValidating, isLoading } =
useSWRInfinite(getKey, fetcher);
// Flatten all pages into a single array
const posts = data ? data.flat() : [];
const isEnd = data && data[data.length - 1]?.length < PAGE_SIZE;
const isLoadingMore = isLoading
|| (size > 0 && data && typeof data[size - 1] === 'undefined');
return (
<div>
{posts.map((post) => (
<article key={post.id}>{post.title}</article>
))}
<button
onClick={() => setSize(size + 1)}
disabled={isLoadingMore || isEnd}
>
{isLoadingMore ? 'Loading...' : isEnd ? 'No more' : 'Load More'}
</button>
</div>
);
}预取数据
SWR 支持多种预取模式,在用户需要之前加载数据。可以在悬停时、路由切换时或应用级别预取。预取的数据存储在缓存中,当组件使用相同 key 挂载时立即返回。
SWR 的 preload 函数触发获取并将结果存入缓存。当组件随后使用相同 key 调用 useSWR 时,缓存数据立即返回,无需加载状态。
import { preload } from 'swr';
// Prefetch on module load
preload('/api/config', fetcher);
// Prefetch on hover
function ProjectLink({ projectId }) {
return (
<a
href={'/projects/' + projectId}
onMouseEnter={() =>
preload('/api/projects/' + projectId, fetcher)
}
>
View Project
</a>
);
}
// The component uses the prefetched data instantly
function ProjectDetail({ projectId }) {
const { data } = useSWR('/api/projects/' + projectId, fetcher);
// If preloaded, data is available immediately
return <div>{data?.name}</div>;
}重新验证策略
SWR 提供多种自动重新验证策略,无需手动干预即可保持数据新鲜。可以全局配置或按 hook 配置。
焦点重新验证
当用户切换回浏览器标签时,SWR 自动重新验证所有活跃数据。确保后台标签中的陈旧数据被刷新。默认启用。
轮询重新验证
设置轮询间隔定期刷新数据。适用于仪表盘、实时动态和实时数据。SWR 会在标签不可见时暂停轮询。
重连重新验证
当浏览器恢复网络连接时,SWR 自动重新验证所有数据。确保离线期间获取的数据在连接恢复后更新。
// Configure revalidation strategies
const { data } = useSWR('/api/stock-price', fetcher, {
// Revalidate on window focus (default: true)
revalidateOnFocus: true,
// Poll every 5 seconds
refreshInterval: 5000,
// Pause polling when tab is hidden
refreshWhenHidden: false,
// Continue polling when offline (default: false)
refreshWhenOffline: false,
// Revalidate on network reconnect (default: true)
revalidateOnReconnect: true,
// Skip revalidation on mount if cached data exists
revalidateIfStale: true,
});
// useSWRImmutable — for data that never changes
import useSWRImmutable from 'swr/immutable';
const { data: config } = useSWRImmutable('/api/config', fetcher);
// Equivalent to:
// useSWR(key, fetcher, {
// revalidateIfStale: false,
// revalidateOnFocus: false,
// revalidateOnReconnect: false,
// })错误处理与重试
SWR 提供内置错误处理和自动重试。当 fetcher 抛出错误时,SWR 捕获它并通过 error 返回值暴露。默认情况下,SWR 使用指数退避重试失败请求。
可以通过 onErrorRetry 自定义重试行为、设置最大重试次数或完全禁用重试。SWR 还支持通过 SWRConfig 中的 onError 回调进行全局错误处理。
import { SWRConfig } from 'swr';
// Global error retry configuration
<SWRConfig
value={{
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// Never retry on 404
if (error.status === 404) return;
// Never retry for a specific key
if (key === '/api/user') return;
// Stop after 10 retries
if (retryCount >= 10) return;
// Retry with exponential backoff
setTimeout(
() => revalidate({ retryCount }),
Math.min(1000 * Math.pow(2, retryCount), 30000)
);
},
}}
>
<App />
</SWRConfig>
// Component-level error handling
function UserData() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, {
errorRetryCount: 3,
errorRetryInterval: 5000,
});
if (error) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<pre>{JSON.stringify(error.info, null, 2)}</pre>
</div>
);
}
return <div>{data?.name}</div>;
}TypeScript 集成
SWR 用 TypeScript 编写,提供开箱即用的优秀类型推断。useSWR hook 是泛型的,允许指定数据类型和错误类型。当提供类型化的 fetcher 时,SWR 自动推断返回类型。
对于更复杂的场景,可以使用 SWRResponse、Key、Fetcher 和其他导出类型在 SWR 之上构建强类型抽象。
import useSWR, { Fetcher, SWRResponse } from 'swr';
// Define your data types
interface User {
id: number;
name: string;
email: string;
}
interface ApiError {
message: string;
status: number;
}
// Typed fetcher
const fetcher: Fetcher<User, string> = async (url) => {
const res = await fetch(url);
if (!res.ok) throw { message: 'Not found', status: res.status };
return res.json();
};
// SWR infers data as User and error as ApiError
function UserProfile({ id }: { id: number }) {
const { data, error } = useSWR<User, ApiError>(
'/api/users/' + id,
fetcher
);
// data is User | undefined
// error is ApiError | undefined
return <div>{data?.name}</div>;
}
// Reusable typed hook
function useUser(id: number): SWRResponse<User, ApiError> {
return useSWR<User, ApiError>('/api/users/' + id, fetcher);
}Next.js 服务端渲染
SWR 与 Next.js 深度集成,支持服务端渲染和静态生成。SWRConfig 中的 fallback 选项让你将服务端获取的数据传递给客户端的 useSWR hook。客户端立即收到预获取的数据,然后在后台重新验证。
对于 App Router(Server Components),可以在服务端获取数据并作为 fallback prop 传递。SWR 使用此数据进行初始渲染并在客户端重新验证。
// app/page.tsx (Server Component)
import { UserList } from './user-list';
export default async function Page() {
// Fetch on the server
const users = await fetch('https://api.example.com/users').then(
(r) => r.json()
);
return (
<UserList fallbackData={users} />
);
}
// app/user-list.tsx (Client Component)
'use client';
import { SWRConfig } from 'swr';
import useSWR from 'swr';
function UserListInner() {
// Uses fallback data on initial render, then revalidates
const { data: users } = useSWR('/api/users', fetcher);
return users?.map((u) => <div key={u.id}>{u.name}</div>);
}
export function UserList({ fallbackData }) {
return (
<SWRConfig value={{ fallback: { '/api/users': fallbackData } }}>
<UserListInner />
</SWRConfig>
);
}缓存与去重
SWR 默认使用内存缓存,以传递给 useSWR 的字符串 key 为键。当多个组件使用相同 key 时,它们共享同一个缓存条目。SWR 自动去重并发请求:如果三个组件同时使用相同 key 挂载,只发送一个网络请求。
可以自定义 cache provider 使用 localStorage、IndexedDB 或任何存储后端。dedupingInterval 选项控制 SWR 将请求视为重复的时长(默认 2 秒)。
import { SWRConfig } from 'swr';
// Custom cache provider with localStorage persistence
function localStorageCacheProvider() {
const map = new Map(
JSON.parse(localStorage.getItem('swr-cache') || '[]')
);
// Save to localStorage before unload
window.addEventListener('beforeunload', () => {
const entries = JSON.stringify(Array.from(map.entries()));
localStorage.setItem('swr-cache', entries);
});
return map;
}
function App() {
return (
<SWRConfig
value={{
provider: localStorageCacheProvider,
dedupingInterval: 5000, // 5-second dedup window
}}
>
<Dashboard />
</SWRConfig>
);
}SWR 中间件
SWR 中间件让你拦截和修改 useSWR hook 的行为。中间件是一个包装 useSWR hook 的函数,在 hook 执行前后可以访问 key、fetcher 和选项。支持日志、分析、请求修改和自定义缓存逻辑。
// Logging middleware
function logger(useSWRNext) {
return (key, fetcher, config) => {
const swr = useSWRNext(key, fetcher, config);
// Log whenever data changes
React.useEffect(() => {
console.log('[SWR]', key, swr.data);
}, [key, swr.data]);
return swr;
};
}
// Auth token injection middleware
function withAuth(useSWRNext) {
return (key, fetcher, config) => {
const authFetcher = (url) => {
const token = getAuthToken();
return fetch(url, {
headers: { Authorization: 'Bearer ' + token },
}).then((r) => r.json());
};
return useSWRNext(key, authFetcher, config);
};
}
// Use middleware globally or per-hook
<SWRConfig value={{ use: [logger, withAuth] }}>
<App />
</SWRConfig>
// Or per-hook
useSWR('/api/data', fetcher, { use: [logger] });测试 SWR 组件
测试使用 SWR 的组件需要将它们包装在带有新 cache provider 的 SWRConfig 中以隔离测试。可以模拟 fetcher、用 fallback 数据预填充缓存,或使用 mock service worker 进行网络级模拟。
import { render, screen, waitFor } from '@testing-library/react';
import { SWRConfig } from 'swr';
// Wrapper to isolate SWR cache between tests
function SWRWrapper({ children }) {
return (
<SWRConfig
value={{
provider: () => new Map(),
dedupingInterval: 0,
}}
>
{children}
</SWRConfig>
);
}
// Test with mock fetcher
test('renders user name', async () => {
const mockUser = { id: 1, name: 'Alice' };
render(
<SWRConfig value={{
provider: () => new Map(),
fallback: { '/api/user': mockUser },
}}>
<UserProfile />
</SWRConfig>
);
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
});
});最佳实践
- 在 SWRConfig 中定义一次 fetcher,而不是在每个 useSWR 调用中重复。
- 使用稳定的 key 字符串。避免用对象或数组构造 key,因为 SWR 使用字符串比较缓存键。
- 使用 useSWRMutation 处理 POST/PUT/DELETE 操作,而不是在 fetch 后手动调用 mutate。
- 为面向用户的变更(点赞、评论、开关)启用乐观更新以改善感知性能。
- 使用 useSWRInfinite 处理分页数据,而不是用 useSWR 手动管理页面状态。
- 设置适当的 dedupingInterval 值,防止快速组件挂载期间的过多网络请求。
- 在测试中用新 Map 缓存提供者的 SWRConfig 包装组件,防止测试污染。
- 利用 SWRConfig 中的 fallback 选项进行 SSR 和服务端数据预加载。
- 使用条件获取(null key)而非在 useSWR 周围添加 if 语句,避免 hooks 顺序问题。
- 使用中间件处理横切关注点,如日志、分析和认证令牌注入。
常见问题
SWR 代表什么?
SWR 代表 stale-while-revalidate,是 RFC 5861 中定义的 HTTP 缓存失效策略。该方法首先返回缓存(陈旧)数据以立即显示,然后在后台获取新数据,最后替换为更新的结果。
SWR 与 React Query(TanStack Query)有什么不同?
SWR 更小(4KB vs 13KB)且 API 更简单,专注于 stale-while-revalidate 模式的数据获取。TanStack Query 提供更多功能,包括内置开发工具、自动垃圾回收、高级变更生命周期和离线变更队列。简单场景选 SWR,复杂数据管理选 TanStack Query。
SWR 是否支持 Next.js App Router 和 Server Components?
是的。在 Server Components 中获取数据并作为 fallback 传递给 Client Component 中的 SWRConfig。SWR 使用服务端数据进行初始渲染并在客户端重新验证。注意 useSWR 本身必须在 Client Components 中使用。
SWR 如何处理请求去重?
当多个组件在 dedupingInterval(默认 2 秒)内使用相同 key 调用 useSWR 时,SWR 只发送一个网络请求。所有组件共享相同的缓存数据,响应到达时同时更新。
SWR 能用于 GraphQL 吗?
可以。SWR 与传输协议无关。可以使用 graphql-request、Apollo Client 或自定义 fetch 包装器作为 fetcher。key 可以是查询字符串或查询与变量的组合。
如何用 SWR 处理分页?
使用 useSWRInfinite hook。它接受一个 getKey 函数,接收页面索引和前一页数据并返回每页的 key。hook 管理页面响应数组并提供 setSize 加载更多页面。
SWR 支持离线模式吗?
SWR 通过缓存层提供基本离线支持。网络离线时缓存数据仍然可用。连接恢复时 SWR 自动重新验证所有数据。要获得高级离线支持和持久缓存,可以实现自定义 cache provider。
如何阻止 SWR 在挂载时获取?
在选项中传递 revalidateOnMount: false。也可以使用 useSWRImmutable(或将 revalidateOnFocus、revalidateOnReconnect 和 revalidateIfStale 都设为 false)。要完全跳过获取,传递 null 作为 key。