跳到主要内容

SWR:优雅的数据请求

一、为什么需要 SWR?

1.1 传统方式的痛点

在学习 SWR 之前,我们先回顾一下用 React 最"原始"的方式请求数据是什么样的。假设我们要获取用户信息:

import { useState, useEffect } from 'react';

interface User {
id: number;
name: string;
email: string;
}

function UserProfile({ userId }: { userId: number }) {
const [data, setData] = useState<User | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
let cancelled = false;
setLoading(true);

fetch(`/api/user/${userId}`)
.then((res) => {
if (!res.ok) throw new Error('请求失败');
return res.json();
})
.then((data) => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch((err) => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});

return () => {
cancelled = true;
};
}, [userId]);

if (loading) return <div>加载中...</div>;
if (error) return <div>出错了:{error.message}</div>;
if (!data) return null;

return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}

看完这段代码,你是不是觉得"就请求个数据,至于写这么多吗?"没错,这就是传统方式的痛点:

痛点说明
代码冗长需要手动管理 loadingerrordata 三个状态,每个请求都要写一遍
没有缓存每次组件挂载都重新发请求,即使数据 1 秒前刚拿过
竞态条件快速切换 userId 时,旧请求可能比新请求晚返回,导致显示错误的数据(上面用 cancelled 变量处理,但很容易忘记)
没有自动重新请求用户切了个标签页刷了会微博,切回来看到的还是 5 分钟前的旧数据
没有去重页面上 Header 和 Sidebar 都要显示用户名,两个组件各发一次相同的请求

1.2 stale-while-revalidate 策略

SWR 的核心思想来自一个叫 stale-while-revalidate 的策略。这个名字听起来很唬人,但用一个"餐厅点餐"的比喻就能秒懂:

想象你去一家常去的餐厅,点了招牌菜红烧肉:

  1. 服务员发现厨房里有一份之前做好的红烧肉(缓存),虽然不是刚出锅的(stale / 过期),但还能吃
  2. 服务员先把这份端上来让你吃着(立即返回缓存数据
  3. 同时通知厨房重新做一份新鲜的(后台重新请求 / revalidate
  4. 新的红烧肉做好了,服务员悄悄把旧的换成新的(更新数据
  5. 你全程没有饿着等——这就是 SWR 的精髓!

用流程图来表示:

名字的由来

stale-while-revalidate 这个名字其实来源于 HTTP 的 Cache-Control 响应头。它是一个标准的 HTTP 缓存策略,告诉浏览器:"缓存过期了也先用着,同时去服务器拿新的。" SWR 库把这个思想搬到了 React 的数据请求中。

1.3 SWR 简介

SWR 是由 Vercel(Next.js 背后的公司)开发的 React 数据请求库:

  • 名字来源于 HTTP 的 stale-while-revalidate 缓存策略
  • 非常轻量,gzip 后仅约 4KB
  • 内置缓存、去重、自动重新验证、错误重试等功能
  • 对 TypeScript 友好,类型推导完善

用 SWR 改写 1.1 中的代码:

import useSWR from 'swr';

interface User {
id: number;
name: string;
email: string;
}

const fetcher = (url: string) => fetch(url).then((res) => res.json());

function UserProfile({ userId }: { userId: number }) {
const { data, error, isLoading } = useSWR<User>(
`/api/user/${userId}`,
fetcher
);

if (isLoading) return <div>加载中...</div>;
if (error) return <div>出错了:{error.message}</div>;
if (!data) return null;

return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}

对比一下:

对比项传统方式SWR
状态管理手动管理 3 个 useState自动提供 data / error / isLoading
缓存自动缓存,相同 key 共享
竞态条件需要手动处理 cancelled自动处理
自动重新请求窗口聚焦、网络恢复时自动请求
请求去重相同 key 自动去重
核心代码量~30 行~5 行

二、基本用法

2.1 安装

# npm
npm install swr

# pnpm
pnpm add swr

# yarn
yarn add swr

2.2 useSWR 基本语法

const { data, error, isLoading } = useSWR(key, fetcher, options);

三个参数的含义:

参数类型必填说明
keystring | array | function | null请求的唯一标识,也会作为参数传给 fetcher
fetcher(key) => Promise<Data>请求函数,接收 key 作为参数,返回数据的 Promise
optionsobject配置项(后面会详细介绍)

2.3 fetcher 函数

SWR 不内置任何请求方法,你需要自己提供 fetcher。这样设计的好处是:你可以用 fetchaxiosGraphQL 甚至任何返回 Promise 的函数。

基于 fetch API:

const fetcher = (url: string) =>
fetch(url).then((res) => {
if (!res.ok) throw new Error('请求失败');
return res.json();
});

// 使用
const { data } = useSWR('/api/user', fetcher);

基于 axios:

import axios from 'axios';

const fetcher = (url: string) => axios.get(url).then((res) => res.data);

// 使用
const { data } = useSWR('/api/user', fetcher);

全局配置 fetcher(推荐):

每个 useSWR 都传 fetcher 太麻烦了,可以用 SWRConfig 全局配置:

import { SWRConfig } from 'swr';

const fetcher = (url: string) =>
fetch(url).then((res) => {
if (!res.ok) throw new Error('请求失败');
return res.json();
});

function App() {
return (
<SWRConfig value={{ fetcher }}>
{/* 子组件中的 useSWR 不再需要传 fetcher */}
<UserProfile />
<Dashboard />
</SWRConfig>
);
}

// 子组件中直接使用,无需传 fetcher
function UserProfile() {
const { data } = useSWR<User>('/api/user');
return <div>{data?.name}</div>;
}

2.4 返回值

useSWR 返回一个对象,包含以下属性:

返回值类型说明
dataT | undefined请求成功返回的数据
errorError | undefined请求抛出的错误
isLoadingboolean首次加载中(无缓存数据且正在请求)
isValidatingboolean正在请求中(包括后台重新验证)
mutatefunction手动更新缓存数据
isLoading 与 isValidating 的区别
  • isLoading首次加载,此时没有缓存数据可以展示,用户看到的是 loading 状态
  • isValidating任何请求进行中,包括首次加载和后台重新验证。即使有缓存数据正在展示,后台偷偷刷新时 isValidating 也为 true

简单记忆:isLoadingisValidating 的子集。

function UserProfile() {
const { data, error, isLoading, isValidating } = useSWR<User>('/api/user');

if (isLoading) return <div>首次加载中...</div>;
if (error) return <div>出错了</div>;

return (
<div>
<h1>{data?.name}</h1>
{isValidating && <span>正在刷新...</span>}
</div>
);
}

2.5 TypeScript 泛型用法

SWR 对 TypeScript 支持非常好,可以通过泛型指定 dataerror 的类型:

// 只指定 data 类型
const { data } = useSWR<User>('/api/user');
// data 的类型为 User | undefined

// 同时指定 data 和 error 类型
const { data, error } = useSWR<User, ApiError>('/api/user');
// data: User | undefined
// error: ApiError | undefined

配合接口定义,可以获得完整的类型提示:

interface ApiResponse<T> {
code: number;
data: T;
message: string;
}

interface User {
id: number;
name: string;
email: string;
}

// fetcher 的返回类型会自动推导
const fetcher = async (url: string): Promise<ApiResponse<User>> => {
const res = await fetch(url);
return res.json();
};

function UserProfile() {
const { data } = useSWR('/api/user', fetcher);
// data 的类型自动推导为 ApiResponse<User> | undefined
return <div>{data?.data.name}</div>;
}

三、核心概念

3.1 缓存与重新验证机制

缓存是 SWR 的灵魂。还记得前面"餐厅点餐"的比喻吗?我们用一张时序图来看看 SWR 内部到底发生了什么:

关键点:

  • 有缓存时:用户立刻看到数据(虽然可能是旧的),体验非常好
  • 请求失败时:不会清空已有的缓存数据,而是同时提供 dataerror,让你自己决定怎么处理
function UserProfile() {
const { data, error, isValidating } = useSWR<User>('/api/user');

return (
<div>
{/* 即使出错,如果有缓存数据仍然展示 */}
{data && <h1>{data.name}</h1>}
{error && <p style={{ color: 'red' }}>刷新失败,显示的是缓存数据</p>}
{isValidating && <p>正在刷新...</p>}
</div>
);
}

3.2 自动重新验证

SWR 会在多种场景下自动重新发起请求,保证数据的新鲜度。就像一个勤快的服务员,时刻关注你的需求:

触发时机配置项默认值说明
窗口聚焦revalidateOnFocustrue用户切换标签页回来时自动刷新
网络恢复revalidateOnReconnecttrue断网恢复后自动刷新
组件挂载revalidateOnMount-组件首次渲染时(默认行为取决于是否有缓存)
定时轮询refreshInterval0(禁用)每隔 N 毫秒自动请求
数据过期revalidateIfStaletrue挂载时如果数据已过期则自动重新验证

配置示例:

// 关闭窗口聚焦时的自动刷新
const { data } = useSWR('/api/user', fetcher, {
revalidateOnFocus: false,
});

// 每 5 秒自动轮询(适合实时性要求高的场景,如股票价格)
const { data } = useSWR('/api/stock/price', fetcher, {
refreshInterval: 5000,
});

// 只在窗口可见时轮询(切到后台时暂停,节省资源)
const { data } = useSWR('/api/notifications', fetcher, {
refreshInterval: 3000,
refreshWhenHidden: false, // 默认就是 false
});

3.3 Key 的设计

Key 是 SWR 最重要的概念——它既是缓存的唯一标识,也是传给 fetcher 的参数。可以把 key 想象成图书馆的索书号:相同的索书号永远指向同一本书。

字符串 key(最常用):

// key 就是 URL,同时作为参数传给 fetcher
const { data } = useSWR('/api/user', fetcher);
// fetcher 收到的参数:'/api/user'

数组 key(需要额外参数时):

// 当 id 变化时,key 变了,SWR 自动重新请求
const { data } = useSWR(['/api/user', id], ([url, id]) =>
fetch(`${url}/${id}`).then((res) => res.json())
);

条件请求(key 为函数或 null):

有时候我们需要"等某个条件满足后再请求",比如先登录再获取用户信息:

// 方式一:key 为 null 时跳过请求
const { data: user } = useSWR(isLoggedIn ? '/api/user' : null, fetcher);

// 方式二:key 为函数,抛出错误或返回 falsy 值时跳过
const { data: orders } = useSWR(
() => (user ? `/api/orders?uid=${user.id}` : null),
fetcher
);
// user 还没加载好时返回 null,SWR 不会发起请求
// user 加载好后自动触发请求
重要提醒

Key 是缓存的唯一标识。相同的 key 在整个应用中共享同一份缓存。这意味着:

  • useSWR('/api/user') 无论在哪个组件调用,拿到的都是同一份数据
  • 如果你需要区分不同的请求,key 必须不同(比如 /api/user/1/api/user/2

3.4 请求去重

当多个组件使用相同的 key 时,SWR 只会发一次请求,所有组件共享结果。这就像一个办公室里好几个人都想喝咖啡,不需要每人都跑一趟咖啡店——派一个人去买,大家一起喝就行了。

// Header 组件
function Header() {
const { data } = useSWR<User>('/api/user');
return <header>欢迎,{data?.name}</header>;
}

// Sidebar 组件
function Sidebar() {
const { data } = useSWR<User>('/api/user');
return <aside>{data?.email}</aside>;
}

// 页面中同时使用两个组件
// 虽然 useSWR('/api/user') 被调用了两次,但只会发一次 HTTP 请求!
function Page() {
return (
<>
<Header />
<Sidebar />
</>
);
}

去重的时间窗口由 dedupingInterval 控制,默认为 2000ms。也就是说,2 秒内对同一个 key 的多次调用只会产生一次实际请求:

// 自定义去重间隔
const { data } = useSWR('/api/user', fetcher, {
dedupingInterval: 5000, // 5 秒内不会重复请求
});

四、常用配置项

前面我们已经零散地见过一些配置项,现在把它们系统地整理一下。你可以把 SWR 的配置想象成汽车的仪表盘——每个旋钮都能微调 SWR 的行为,让它完美适配你的业务场景。

4.1 配置项速查表

配置项类型默认值说明
revalidateOnFocusbooleantrue窗口聚焦时自动重新验证
revalidateOnReconnectbooleantrue网络恢复时自动重新验证
refreshIntervalnumber0轮询间隔(毫秒),0 表示禁用
dedupingIntervalnumber2000去重时间窗口(毫秒),同一 key 在此时间内只请求一次
errorRetryCountnumber5最大错误重试次数
errorRetryIntervalnumber5000错误重试间隔(毫秒),实际会使用指数退避
fallbackDataany-初始数据,在缓存为空时作为占位数据返回
keepPreviousDatabooleanfalse切换 key 时保留上一个 key 的数据,直到新数据返回
revalidateIfStalebooleantrue挂载时如果缓存数据已过期,是否自动重新验证
shouldRetryOnErrorbooleantrue请求出错时是否自动重试
suspensebooleanfalse启用 React Suspense 模式

4.2 全局配置 SWRConfig

在 2.3 节中我们用 SWRConfig 全局配置了 fetcher,其实所有配置项都可以通过它来全局设置,避免在每个 useSWR 中重复书写。

基本用法:

import { SWRConfig } from 'swr';

function App() {
return (
<SWRConfig
value={{
fetcher: (url: string) => fetch(url).then((res) => res.json()),
revalidateOnFocus: false,
errorRetryCount: 3,
}}
>
<Dashboard />
</SWRConfig>
);
}

嵌套 Provider——配置会自动合并:

就像 CSS 的层叠一样,内层的 SWRConfig 会继承并覆盖外层的配置:

<SWRConfig value={{ revalidateOnFocus: false, refreshInterval: 0 }}>
{/* 这里的组件:revalidateOnFocus=false, refreshInterval=0 */}
<SWRConfig value={{ refreshInterval: 3000 }}>
{/* 这里的组件:revalidateOnFocus=false(继承), refreshInterval=3000(覆盖) */}
<RealTimePanel />
</SWRConfig>
</SWRConfig>

函数式配置——基于父级配置扩展:

如果你想基于父级配置做修改而不是完全覆盖,可以传入一个函数:

<SWRConfig
value={(parentConfig) => ({
...parentConfig,
// 在父级的重试次数基础上 +2
errorRetryCount: (parentConfig?.errorRetryCount ?? 5) + 2,
})}
>
<CriticalSection />
</SWRConfig>

4.3 配置示例

下面是几个实用的配置组合,覆盖常见业务场景:

场景一:关闭所有自动重新验证(适合静态数据)

有些数据几乎不会变(比如国家列表、配置字典),没必要反复请求:

const { data } = useSWR('/api/countries', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
// 相当于"请求一次就够了,别再烦服务器了"
});

场景二:实时数据轮询(适合股票、通知等)

const { data } = useSWR('/api/stock/price', fetcher, {
refreshInterval: 1000, // 每秒轮询
dedupingInterval: 500, // 缩短去重窗口,保证轮询频率
revalidateOnFocus: true, // 切回来立刻刷新
});

场景三:错误重试配置

const { data } = useSWR('/api/payment/status', fetcher, {
errorRetryCount: 10, // 最多重试 10 次
errorRetryInterval: 3000, // 基础重试间隔 3 秒(实际会指数退避)
shouldRetryOnError: true, // 开启重试
});

五、错误处理与加载状态

数据请求不可能永远一帆风顺——网络会断、服务器会挂、接口会超时。SWR 提供了一套完善的错误处理和加载状态管理机制,让你的应用在各种"意外"面前都能优雅应对。

5.1 错误重试机制

SWR 默认会在请求失败时自动重试,而且使用的是指数退避(Exponential Backoff)策略。什么意思呢?就像你打电话没人接,第一次等 1 秒再打,第二次等 2 秒,第三次等 4 秒……间隔越来越长,避免"夺命连环 call"把服务器打崩。

默认重试行为:

重试次数大约等待时间说明
第 1 次~5s基础间隔
第 2 次~10s指数退避
第 3 次~20s指数退避
第 4 次~40s指数退避
第 5 次~60s(上限)默认最多重试 5 次

自定义重试逻辑(onErrorRetry):

const { data } = useSWR('/api/user', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 404 不重试——资源不存在,重试也没用
if (error.status === 404) return;
// 最多重试 3 次
if (retryCount >= 3) return;
// 5 秒后重试
setTimeout(() => revalidate({ retryCount }), 5000);
},
});

禁用重试:

const { data } = useSWR('/api/user', fetcher, {
shouldRetryOnError: false,
});

5.2 条件请求

在 3.3 节中我们简单提过用 null key 跳过请求,这里展开讲讲实际应用场景。

条件请求的核心思想:当 key 为 null(或函数返回 falsy 值)时,SWR 不会发起请求。这就像快递员看到收件地址是空的,自然不会出发。

场景:未登录时不请求用户信息

interface User {
id: number;
name: string;
}

function UserProfile() {
const token = useAuthToken(); // 可能为 null(未登录)

// token 为 null 时,key 为 null,SWR 跳过请求
const { data } = useSWR<User>(
token ? '/api/user/profile' : null,
fetcher
);

if (!token) return <div>请先登录</div>;
return <div>你好,{data?.name}</div>;
}

场景:搜索框输入为空时不请求

function SearchResults({ query }: { query: string }) {
// query 为空字符串时不请求
const { data } = useSWR(
query ? `/api/search?q=${encodeURIComponent(query)}` : null,
fetcher
);

if (!query) return <div>请输入搜索关键词</div>;
return <ul>{data?.map((item: string) => <li key={item}>{item}</li>)}</ul>;
}

5.3 依赖请求

有时候一个请求需要用到另一个请求的结果,就像做菜一样——得先买好菜(请求 A),才能下锅炒(请求 B)。SWR 通过条件请求天然支持这种"链式依赖"。

示例:先获取用户信息,再根据用户 ID 获取订单列表

function UserOrders() {
// 第一步:获取用户信息
const { data: user } = useSWR<User>('/api/user', fetcher);

// 第二步:用户信息拿到后,再获取订单
// user 为 undefined 时,key 为 null,不会发起请求
const { data: orders } = useSWR<Order[]>(
user ? `/api/orders?uid=${user.id}` : null,
fetcher
);

if (!user) return <div>加载用户信息...</div>;
if (!orders) return <div>加载订单列表...</div>;

return (
<div>
<h2>{user.name} 的订单</h2>
<ul>
{orders.map((order) => (
<li key={order.id}>订单 #{order.id} - ¥{order.amount}</li>
))}
</ul>
</div>
);
}

用流程图展示依赖链:

依赖链可以更长

你可以串联任意多个依赖请求,SWR 会自动按顺序"瀑布式"执行。但要注意,链越长用户等待越久,尽量让后端提供聚合接口减少请求链。

5.4 isLoading vs isValidating 详解

这两个状态经常让人困惑,我们用一张表格彻底搞清楚它们在不同场景下的值:

场景isLoadingisValidatingdata
首次请求(无缓存)truetrueundefined
请求完成falsefalse有值
后台重新验证falsetrue有值(缓存)
切换 key(无新 key 缓存)truetrueundefined
切换 key(有新 key 缓存)falsetrue有值(缓存)

简单来说:

  • isLoading = 用户需要看到 loading 骨架屏的时刻(没有任何数据可展示)
  • isValidating = 后台正在干活(可能有缓存数据正在展示,也可能没有)
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, isValidating } = useSWR<User>(
`/api/user/${userId}`,
fetcher,
{ keepPreviousData: true }
);

return (
<div>
{/* isLoading 时显示骨架屏 */}
{isLoading && <Skeleton />}

{/* 有数据时正常渲染 */}
{data && <h1>{data.name}</h1>}

{/* isValidating 时在角落显示一个小 spinner */}
{isValidating && !isLoading && <MiniSpinner />}
</div>
);
}
keepPreviousData 的妙用

上面的例子中使用了 keepPreviousData: true,这样切换 userId 时不会闪一下 loading,而是保留上一个用户的数据直到新数据返回。用户体验更丝滑,就像翻书而不是每次都合上再打开。


六、数据变更(Mutation)

前面五章我们一直在讲"读"数据——从服务器获取数据并展示。但真实应用中,用户还需要"写"数据:提交表单、点赞、删除评论……写完之后,页面上显示的数据就过时了,需要更新。

这就像你在图书馆的借阅系统里还了一本书(写操作),但屏幕上还显示"已借出"(旧缓存)。你需要一种方式告诉系统:"嘿,数据变了,刷新一下!"这就是 Mutation 要解决的问题。

6.1 什么是 Mutation

SWR 提供了 mutate 函数来更新缓存。获取 mutate 有两种方式:

方式来源作用范围使用场景
绑定 mutateuseSWR 的返回值只能操作当前 key 的缓存在同一组件中修改并刷新数据
全局 mutateuseSWRConfig()可以操作任意 key 的缓存在 A 组件中修改数据后刷新 B 组件的缓存
import useSWR, { useSWRConfig } from 'swr';

function MyComponent() {
// 方式一:绑定 mutate(只能操作 '/api/user' 的缓存)
const { data, mutate } = useSWR('/api/user', fetcher);

// 方式二:全局 mutate(可以操作任意 key)
const { mutate: globalMutate } = useSWRConfig();
}

6.2 绑定 mutate

useSWR 返回的 mutate 已经绑定了当前的 key,使用起来最简单。

基本用法:

const { data, mutate } = useSWR('/api/todos', fetcher);

// 用法一:重新验证——告诉 SWR "去服务器拿最新数据"
await mutate();

// 用法二:直接更新缓存——不发请求,直接把缓存改成新值
await mutate(newTodos, { revalidate: false });

参数说明:

参数类型说明
dataT | Promise<T> | (currentData) => T新数据、返回新数据的 Promise、或基于当前数据的更新函数
options.revalidateboolean更新缓存后是否重新请求服务器验证,默认 true
options.populateCacheboolean是否将 mutate 的返回值写入缓存,默认 true
options.optimisticDataT | (currentData) => T乐观更新的临时数据
options.rollbackOnErrorboolean请求失败时是否回滚乐观更新,默认 true

6.3 全局 mutate

当你需要在一个组件中修改数据,然后刷新另一个组件的缓存时,就需要全局 mutate

import { useSWRConfig } from 'swr';

function DeleteUserButton({ userId }: { userId: number }) {
const { mutate } = useSWRConfig();

const handleDelete = async () => {
await fetch(`/api/user/${userId}`, { method: 'DELETE' });

// 删除用户后,刷新用户列表(另一个组件的数据)
mutate('/api/users');

// 也可以同时刷新多个 key
mutate('/api/stats');
};

return <button onClick={handleDelete}>删除用户</button>;
}

全局 mutate 还支持key 过滤函数,一次性刷新匹配的所有缓存:

const { mutate } = useSWRConfig();

// 刷新所有以 '/api/user' 开头的缓存
mutate((key) => typeof key === 'string' && key.startsWith('/api/user'));

6.4 乐观更新

乐观更新是一种"先斩后奏"的策略:先在界面上更新数据,再发请求到服务器。如果请求失败,就回滚到之前的状态。

为什么要这样做?因为大多数操作都会成功,让用户等服务器响应后再更新界面会显得很"卡"。就像你在社交媒体点赞,点完立刻变红心(乐观更新),而不是转圈等 500ms 后才变——虽然请求还在路上,但用户已经得到了即时反馈。

完整示例:点赞按钮

interface Post {
id: number;
title: string;
likes: number;
isLiked: boolean;
}

function LikeButton({ postId }: { postId: number }) {
const { data: post, mutate } = useSWR<Post>(
`/api/posts/${postId}`,
fetcher
);

const handleLike = async () => {
if (!post) return;

// 乐观更新:立刻在界面上 +1
await mutate(
// 发送请求到服务器
fetch(`/api/posts/${postId}/like`, { method: 'POST' })
.then((res) => res.json()),
{
// 乐观数据:不等服务器,先更新界面
optimisticData: {
...post,
likes: post.likes + 1,
isLiked: true,
},
// 请求失败时自动回滚到之前的数据
rollbackOnError: true,
// 请求成功后不再重新验证(服务器返回的就是最新数据)
revalidate: false,
}
);
};

return (
<button onClick={handleLike}>
{post?.isLiked ? '❤️' : '🤍'} {post?.likes ?? 0}
</button>
);
}

乐观更新的执行流程:

6.5 useSWRMutation

前面的 useSWR 是为"读"数据设计的——组件挂载时自动请求。但对于"写"操作(POST、PUT、DELETE),我们不希望自动触发,而是等用户点击按钮后才执行。这就是 useSWRMutation 的用武之地。

useSWR vs useSWRMutation:

对比项useSWRuseSWRMutation
触发方式自动(组件挂载时)手动(调用 trigger
适用场景GET 请求(读数据)POST/PUT/DELETE(写数据)
返回的状态isLoading / isValidatingisMutating
缓存行为自动缓存和重新验证不自动缓存,可手动更新关联缓存

完整示例:表单提交

import useSWRMutation from 'swr/mutation';

interface NewTodo {
title: string;
completed: boolean;
}

// 注意:fetcher 的参数格式与 useSWR 不同
// 第一个参数是 key,第二个参数是 { arg }(通过 trigger 传入)
async function createTodo(url: string, { arg }: { arg: NewTodo }) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(arg),
});
if (!res.ok) throw new Error('创建失败');
return res.json();
}

function AddTodoForm() {
const [title, setTitle] = useState('');

const { trigger, isMutating, error } = useSWRMutation(
'/api/todos',
createTodo
);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await trigger({ title, completed: false });
setTitle(''); // 成功后清空输入框
} catch {
// error 已经被 useSWRMutation 捕获并存储
}
};

return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="输入待办事项"
disabled={isMutating}
/>
<button type="submit" disabled={isMutating}>
{isMutating ? '提交中...' : '添加'}
</button>
{error && <p style={{ color: 'red' }}>{error.message}</p>}
</form>
);
}

返回值速查:

返回值类型说明
trigger(arg, options?)function手动触发请求,arg 会传给 fetcher 的 { arg } 参数
dataT | undefined最近一次成功请求的返回数据
errorError | undefined最近一次请求的错误
isMutatingboolean是否正在请求中
resetfunction重置 dataerror 状态
配合 mutate 刷新列表

useSWRMutation 创建数据后,通常需要刷新列表。可以在 trigger 成功后调用全局 mutate 刷新关联的 key:

const { mutate } = useSWRConfig();
const { trigger } = useSWRMutation('/api/todos', createTodo);

const handleAdd = async () => {
await trigger({ title: '新待办', completed: false });
// 刷新列表
mutate('/api/todos');
};

七、高级用法

7.1 分页与无限加载(useSWRInfinite)

在社交媒体、电商列表等场景中,数据往往不是一次性加载完的,而是"滚到底部加载更多"或者点击"下一页"。这就像吃自助餐——你不会一次把所有菜都端到桌上,而是吃完一盘再去拿一盘。

useSWRInfinite 就是 SWR 为这种场景量身打造的 Hook。

基本语法:

import useSWRInfinite from 'swr/infinite';

const { data, size, setSize, isValidating, isLoading } = useSWRInfinite(
getKey,
fetcher,
options
);

核心概念——getKey 函数:

getKeyuseSWRInfinite 最关键的参数。它接收当前页码和上一页的数据,返回当前页的请求 key。就像翻书时,你需要知道"当前是第几页"以及"上一页最后一条是什么"才能翻到下一页。

// getKey 的签名
(pageIndex: number, previousPageData: T[] | null) => string | null

完整示例:带"加载更多"按钮的列表

import useSWRInfinite from 'swr/infinite';

interface Article {
id: number;
title: string;
}

const PAGE_SIZE = 10;

function ArticleList() {
const getKey = (pageIndex: number, previousPageData: Article[] | null) => {
// 首页:previousPageData 为 null
// 到达末尾:上一页返回的数据不足 PAGE_SIZE,说明没有更多了
if (previousPageData && previousPageData.length < PAGE_SIZE) return null;
return `/api/articles?page=${pageIndex}&limit=${PAGE_SIZE}`;
};

const { data, size, setSize, isValidating, isLoading } =
useSWRInfinite<Article[]>(getKey, fetcher);

// data 是二维数组:[[第0页数据], [第1页数据], ...]
const articles = 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>
{articles.map((article) => (
<div key={article.id}>{article.title}</div>
))}

{isEnd ? (
<p>没有更多了</p>
) : (
<button
onClick={() => setSize(size + 1)}
disabled={isLoadingMore || isValidating}
>
{isLoadingMore ? '加载中...' : '加载更多'}
</button>
)}
</div>
);
}

返回值速查:

返回值类型说明
dataT[][]二维数组,每个元素是一页的数据
sizenumber当前应该加载的页数(从 1 开始)
setSize(size) => void设置需要加载的页数,触发新页请求
isValidatingboolean是否有任何一页正在请求
isLoadingboolean首次加载中
mutatefunction手动更新所有页的缓存

7.2 预请求(Prefetching)

预请求就像餐厅里的"提前备菜"——在客人还没点菜之前,厨房就把热门菜品准备好了。当用户真正需要数据时,缓存里已经有了,体验飞快。

SWR 提供了 preload 函数来实现预请求:

import { preload } from 'swr';
import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

// 场景:鼠标悬停时预请求,点击后立即展示
function UserCard({ userId }: { userId: number }) {
const handleMouseEnter = () => {
// 鼠标悬停时提前加载用户详情
preload(`/api/user/${userId}`, fetcher);
};

return (
<div onMouseEnter={handleMouseEnter}>
<a href={`/user/${userId}`}>查看用户详情</a>
</div>
);
}

// 用户详情页:数据已经在缓存中,无需等待
function UserDetail({ userId }: { userId: number }) {
const { data } = useSWR(`/api/user/${userId}`, fetcher);
// 如果用户之前悬停过,data 立即可用,不会出现 loading
return <div>{data?.name}</div>;
}
预请求的最佳时机
  • 鼠标悬停在链接上时(用户大概率会点击)
  • 路由预加载时(如 Next.js 的 <Link> 组件进入视口时)
  • 页面空闲时预加载下一步可能需要的数据

7.3 SSR/SSG 支持

在服务端渲染(SSR)或静态生成(SSG)场景中,我们希望页面在服务端就带上数据,而不是到了浏览器再请求。SWR 通过 fallback 配置完美支持这一需求。

原理: 在服务端获取数据,通过 SWRConfigfallback 注入到缓存中。客户端渲染时,SWR 发现缓存里已经有数据了,就直接使用,不会出现 loading 闪烁。

在 Next.js 中使用(getServerSideProps):

import { SWRConfig } from 'swr';

export async function getServerSideProps() {
const user = await fetch('https://api.example.com/user').then((res) =>
res.json()
);

return {
props: {
fallback: {
'/api/user': user,
},
},
};
}

export default function Page({ fallback }: { fallback: Record<string, any> }) {
return (
<SWRConfig value={{ fallback }}>
<UserProfile />
</SWRConfig>
);
}

// 子组件正常使用 useSWR,首次渲染直接拿到 fallback 数据
function UserProfile() {
const { data } = useSWR('/api/user', fetcher);
return <div>{data?.name}</div>;
}

在 Next.js 中使用(getStaticProps / SSG):

export async function getStaticProps() {
const articles = await fetch('https://api.example.com/articles').then(
(res) => res.json()
);

return {
props: {
fallback: {
'/api/articles': articles,
},
},
revalidate: 60, // ISR:每 60 秒重新生成
};
}

7.4 中间件

SWR 的中间件机制就像洋葱模型——每个中间件包裹在 useSWR 外面,可以在请求前后插入自定义逻辑。你可以用它来做日志、性能监控、数据转换等。

中间件的基本结构:

import type { Middleware } from 'swr';

const myMiddleware: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
// 请求前的逻辑...
const swr = useSWRNext(key, fetcher, config);
// 请求后的逻辑...
return swr;
};
};

示例:日志中间件

import type { Middleware } from 'swr';

const logger: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
const swr = useSWRNext(key, fetcher, config);

if (swr.error) {
console.error(`[SWR Error] ${key}:`, swr.error);
}
if (swr.data) {
console.log(`[SWR Data] ${key}:`, swr.data);
}

return swr;
};
};

// 使用:全局配置或单个 Hook 配置
<SWRConfig value={{ use: [logger] }}>
<App />
</SWRConfig>

// 或单个 Hook
const { data } = useSWR('/api/user', fetcher, { use: [logger] });
常用官方中间件
  • swr/immutable:不可变数据模式,关闭所有自动重新验证,适合不会变化的数据
  • swr/infinite:分页/无限加载(即 useSWRInfinite
  • swr/mutation:数据变更(即 useSWRMutation

社区也有很多第三方中间件,如 swr-devtools(开发者工具)等。


八、SWR vs React Query(TanStack Query)

SWR 和 React Query 是 React 生态中最流行的两个数据请求库,经常被拿来比较。它们就像轿车和 SUV——都能把你从 A 点送到 B 点,但适合的路况不同。

对比项SWRReact Query (TanStack Query)
包大小~4KB (gzip)~13KB (gzip)
API 风格极简,一个 Hook 搞定大部分场景功能丰富,API 更多更细
DevTools社区方案(swr-devtools官方内置,功能强大
缓存策略基于 key 的内存 Map基于 key 的内存缓存 + 垃圾回收
Mutation 支持mutate + useSWRMutationuseMutation,功能更完善
分页支持useSWRInfiniteuseInfiniteQuery,支持双向滚动
离线支持基础(缓存可用)完善(离线队列、持久化缓存)
学习曲线低,30 分钟上手中等,概念更多
社区生态活跃,Vercel 维护非常活跃,TanStack 生态
适用场景轻量项目、Next.js 项目复杂数据管理、企业级应用

选择建议:

SWR 如果你:

  • 项目较轻量,数据请求逻辑不复杂
  • 已经在使用 Next.js(SWR 和 Next.js 同属 Vercel 生态,配合天衣无缝)
  • 追求极简 API,不想引入太多概念
  • 包体积敏感(4KB vs 13KB)

React Query 如果你:

  • 项目数据流复杂,需要精细的缓存控制和垃圾回收
  • 需要强大的 DevTools 来调试缓存状态
  • 需要离线支持、持久化缓存等高级功能
  • 团队规模较大,需要更规范的数据管理模式
一句话总结

SWR 是"小而美"的瑞士军刀,React Query 是"大而全"的工具箱。小项目用 SWR 更轻快,大项目用 React Query 更省心。


九、总结与 API 速查表

9.1 核心 Hooks

Hook用途基本语法
useSWR数据获取(读)const { data, error, isLoading } = useSWR(key, fetcher)
useSWRMutation数据变更(写)const { trigger, isMutating } = useSWRMutation(key, fetcher)
useSWRInfinite分页 / 无限加载const { data, size, setSize } = useSWRInfinite(getKey, fetcher)

9.2 常用配置速查

配置项默认值说明
revalidateOnFocustrue窗口聚焦时重新验证
revalidateOnReconnecttrue网络恢复时重新验证
refreshInterval0轮询间隔(ms),0 为禁用
dedupingInterval2000去重时间窗口(ms)
errorRetryCount5最大重试次数
fallbackData-初始占位数据
keepPreviousDatafalse切换 key 时保留旧数据
suspensefalse启用 React Suspense 模式

9.3 常用返回值速查

返回值来源说明
datauseSWR / useSWRMutation请求成功返回的数据
erroruseSWR / useSWRMutation请求错误
isLoadinguseSWR首次加载中(无缓存)
isValidatinguseSWR任何请求进行中
mutateuseSWR / useSWRConfig手动更新缓存
triggeruseSWRMutation手动触发写操作
isMutatinguseSWRMutation写操作进行中
size / setSizeuseSWRInfinite当前页数 / 设置页数

十、面试常见问题

Q1: SWR 的 stale-while-revalidate 策略是什么?

答: SWR 的核心思想可以用三步概括:

  1. Stale(过期):当组件请求数据时,SWR 先检查缓存。如果缓存中有数据(即使可能是"过期"的),立即返回给组件渲染,用户不用等待。
  2. While(同时):在返回缓存数据的同时,SWR 在后台发起一个新的网络请求去服务器获取最新数据。
  3. Revalidate(重新验证):当后台请求返回后,用新数据更新缓存并触发组件重新渲染。

这个策略的精髓在于用户永远不会看到空白的 loading 状态(只要有缓存),同时数据又能保持最新。就像你打开新闻 App,先看到上次缓存的新闻列表,几百毫秒后列表悄悄刷新成最新的——体验非常丝滑。

Q2: SWR 如何处理竞态条件?

答: 竞态条件是指:快速切换参数时,先发出的请求可能比后发出的请求更晚返回,导致界面显示了旧数据。

SWR 内部通过 key 版本控制 自动解决这个问题。每次 key 变化时,SWR 会记录当前的 key 版本。当请求返回时,SWR 会检查返回的数据是否属于当前版本的 key——如果 key 已经变了(说明用户已经切换到了新的请求),旧请求的响应会被自动丢弃,不会更新缓存或触发渲染。

举个例子:用户快速从"用户 A"切换到"用户 B",即使"用户 A"的请求比"用户 B"晚返回,SWR 也只会展示"用户 B"的数据。开发者无需手动处理 AbortControllercancelled 标志。

Q3: useSWR 的 key 设计有哪些注意事项?

答: Key 是 SWR 最核心的概念,需要注意以下几点:

要点说明
缓存标识Key 是缓存的唯一标识,相同 key 在整个应用中共享同一份缓存数据
传给 fetcherKey 同时作为参数传给 fetcher 函数,通常就是请求的 URL
null 跳过请求当 key 为 null 时,SWR 不会发起请求,常用于条件请求
数组 key需要额外参数时使用数组 key,如 ['/api/user', id],任一元素变化都会触发重新请求
稳定性避免在渲染中创建新的对象/数组作为 key(引用不同会导致无限请求),应使用字符串或稳定的序列化值
唯一性不同的数据必须使用不同的 key,否则会互相覆盖缓存

Q4: 如何实现乐观更新?

答: 乐观更新是一种"先更新界面,再发请求"的策略,通过 mutateoptimisticDatarollbackOnError 实现:

const { data, mutate } = useSWR<Todo[]>('/api/todos', fetcher);

const handleToggle = async (todo: Todo) => {
const updated = { ...todo, completed: !todo.completed };

await mutate(
fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: JSON.stringify(updated),
}).then((res) => res.json()),
{
// 立即在界面上更新,不等服务器响应
optimisticData: data?.map((t) => (t.id === todo.id ? updated : t)),
// 请求失败时自动回滚到修改前的数据
rollbackOnError: true,
revalidate: false,
}
);
};

执行流程:用户操作 -> 界面立即更新(optimisticData)-> 发送请求到服务器 -> 成功则保持,失败则回滚(rollbackOnError)。这样用户感受到的是"零延迟"的交互体验。

Q5: SWR 和 React Query 的主要区别?

答: 两者的核心区别可以从三个维度理解:

维度SWRReact Query
定位轻量级数据请求库全功能服务端状态管理库
体积~4KB gzip~13KB gzip
功能覆盖 80% 常见场景覆盖 99% 场景,含离线支持、垃圾回收、DevTools

简单来说:如果你的项目数据请求逻辑简单、追求轻量,选 SWR;如果数据流复杂、需要精细控制缓存生命周期和离线能力,选 React Query。两者都是优秀的库,没有绝对的好坏之分。

Q6: SWR 的缓存是如何工作的?

答: SWR 的缓存机制有以下特点:

  1. 基于 key 的内存缓存:SWR 内部维护一个 Map 结构,key 是请求标识,value 是对应的数据。所有使用相同 key 的 useSWR 调用共享同一份缓存。

  2. 全局共享:缓存是全局的。组件 A 和组件 B 如果使用相同的 key,它们拿到的是同一份数据,且只会发一次请求(去重机制)。

  3. 生命周期:缓存存在于内存中,页面刷新后清空。如果需要持久化,可以通过自定义 provider 将缓存存储到 localStorage 等持久化存储中。

  4. 自定义缓存 Provider

import { SWRConfig } from 'swr';

function localStorageProvider() {
const map = new Map<string, any>(
JSON.parse(localStorage.getItem('swr-cache') || '[]')
);

window.addEventListener('beforeunload', () => {
const entries = JSON.stringify(Array.from(map.entries()));
localStorage.setItem('swr-cache', entries);
});

return map;
}

function App() {
return (
<SWRConfig value={{ provider: localStorageProvider }}>
<Dashboard />
</SWRConfig>
);
}

这样即使用户刷新页面,缓存数据也不会丢失,首次渲染就能立即展示上次的数据。