切换语言
切换主题

SWR 完全指南:掌握缓存策略和乐观更新的实战技巧

周五下午三点,我盯着屏幕上那个熟悉的loading转圈,第N次刷新用户列表页面。每次切换tab,loading;每次从别的页面回来,loading;哪怕只是因为接了个电话导致浏览器失焦,回来又是loading。最抓狂的是什么?那些数据明明一秒钟前才加载过。

我猜你也遇到过这种情况——写React项目时,每个数据获取都得写一遍useState、useEffect,再加上loading、error的处理逻辑。20行代码打底。更头疼的是,几个组件需要同一份数据,你要么把状态提升到父组件(写更多代码),要么每个组件各自请求(浪费网络资源)。

说实话,那段时间我一度怀疑是不是我的姿势不对。后来发现SWR之后,那种”卧槽还能这样”的感觉,大概就跟第一次用Git替代手动复制文件夹差不多。三行代码,解决了80%的问题。不夸张。

今天就聊聊SWR——Vercel出品的React数据获取库。核心就一个词:stale-while-revalidate(陈旧-重新验证)。听起来很技术?其实原理超简单:先给你看缓存的”旧照片”(快),后台偷偷拍新的(准),拍完了悄悄换上(无感)。

为什么需要SWR?传统数据获取的三大痛点

我们先看看传统的React数据获取是什么样的。假设你要显示一个用户列表:

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

24行代码。只是为了显示一个列表。你可能觉得还行?那再想想这些场景:

痛点1:重复到想吐的模板代码

每个需要数据的组件都得这么写。用户列表要写,文章列表要写,评论列表还要写。三个状态、一堆if判断、错误处理…复制粘贴都觉得丢人。有次我数了数,一个中型项目里,光这种模板代码就占了20%的代码量。

痛点2:缓存?不存在的

最扎心的是没有缓存。用户从首页跳到详情页,再返回首页——又是漫长的loading。数据明明30秒前才拿过,却要重新请求。用户抱怨”你们这网站怎么这么慢”,其实不是API慢,是我们压根没缓存。

有人说可以自己写个全局状态管理啊。嗯,可以。然后你就要维护Redux/Zustand的store、action、reducer…原本3分钟能写完的功能,变成了30分钟的架构设计。

痛点3:多组件数据同步是噩梦

更麻烦的是多个组件用同一份数据。比如顶部导航要显示未读消息数,侧边栏也要显示,消息列表页还要显示。怎么办?状态提升到最顶层?那每次更新都要传十几层props。用Context?那每次消息更新,整个树都要re-render。

我记得有次改一个消息通知功能,明明逻辑很简单,却花了两天处理状态同步问题。提交代码的时候,满屏都是setState和useEffect,自己都看不下去。

这就是为什么SWR能火起来。它不是又一个轮子,而是真的解决了痛点。用SWR重写上面的代码?

import useSWR from 'swr';

function UserList() {
  const { data, error, isLoading } = useSWR('/api/users', fetcher);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;

  return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

9行代码。搞定。而且自带缓存、自动revalidate、跨组件共享。对了,fetcher就是你的fetch函数,通常全局定义一次就够了:

const fetcher = url => fetch(url).then(r => r.json());

这差距,一目了然。

SWR的核心概念:Stale-While-Revalidate策略

SWR这个名字来自HTTP RFC 5861定义的缓存失效策略。别被”RFC”吓到,原理超好理解。

用照片打个比方:你想看朋友的近照。传统方式是——打电话让他拍一张发给你,你等着。SWR的方式是——先翻出上次见面时拍的老照片(可能是上周的),边看边给他发消息让他拍新的发过来,收到新照片后再替换。

关键差别在哪?你不用干等。立即能看到内容(虽然可能有点旧),同时确保最终看到的是最新的。这就是”stale-while-revalidate”——展示旧的,同时重新验证。

SWR的完整工作流程

  1. 第一步:立即返回缓存(stale)

    • 组件挂载时,SWR先检查本地缓存
    • 有缓存?立即返回,页面秒开
    • 没缓存?返回undefined,显示loading
  2. 第二步:后台请求(revalidate)

    • 无论是否有缓存,都会发起API请求
    • 用户感知不到,因为已经在看内容了
  3. 第三步:更新数据

    • API返回后,悄悄更新缓存和UI
    • 如果数据变了,React自动re-render
    • 如果没变,什么都不做

来看个实际例子。我在做一个股票价格展示页面:

function StockPrice({ symbol }) {
  const { data, error } = useSWR(`/api/stock/${symbol}`, fetcher);

  return (
    <div>
      <h2>{symbol}</h2>
      <p>价格: {data ? `$${data.price}` : 'Loading...'}</p>
      <span>更新时间: {data?.updatedAt}</span>
    </div>
  );
}

用户第一次打开页面,data是undefined,显示”Loading…”。一秒钟后API返回,显示价格。

用户切到别的tab看了会邮件,10分钟后切回来——瞬间就看到价格(来自缓存),loading都没闪。但与此同时,SWR已经在后台发起新请求。如果股价变了,页面会自动更新;没变就保持原样。

这个体验,丝滑。

自动revalidation的三种时机

SWR默认会在这些情况下自动重新获取数据:

  1. 组件重新挂载(revalidateOnMount):刷新页面或路由切换回来时
  2. 窗口重新聚焦(revalidateOnFocus):用户切回浏览器tab时
  3. 网络恢复(revalidateOnReconnect):断网后重新联网时

这意味着什么?你啥都不用管,SWR自动帮你保持数据新鲜。用户看邮件看了半小时,切回你的网站——SWR自动刷新数据。用户在地铁里断网了,出站恢复4G——SWR自动重新加载。

说实话,第一次知道这些是默认行为时,我直接惊了。以前这些场景要么不处理(用户看到过期数据),要么手动监听visibilitychange、online事件,写一堆代码。现在?免费送的。

关键的key概念

你可能注意到了,useSWR的第一个参数是'/api/users'这样的字符串。这个叫”key”,非常重要。

// 两个组件用同一个key
function Header() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>欢迎, {data?.name}</div>;
}

function Profile() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>个人资料: {data?.email}</div>;
}

只要key相同,数据就会共享。SWR只会请求一次API,两个组件都拿到相同的数据,还会自动同步更新。

这就是为什么说它解决了”多组件数据同步”的问题。不需要状态管理,不需要Context,就是一个key的事儿。

缓存策略深入:让数据获取更智能

前面说了SWR会自动缓存、自动revalidate。但实际项目中,不同数据的”新鲜度要求”是不一样的。用户头像可能一周都不变,股票价格却要秒级更新。这时候就需要调整缓存策略。

默认行为很聪明,但不是万能的

SWR默认的配置其实挺激进的:

  • 每次组件挂载都revalidate
  • 每次窗口聚焦都revalidate
  • 每次网络恢复都revalidate

对于实时性要求高的数据(聊天消息、在线状态),这很完美。但对于相对稳定的数据(文章列表、用户资料),就有点浪费了。用户在两个tab之间来回切换,每次都发请求?没必要。

核心配置选项

SWR提供了一堆配置项,但你真正常用的就这几个:

const { data } = useSWR('/api/articles', fetcher, {
  revalidateOnFocus: false,      // 窗口聚焦时不重新请求
  revalidateOnReconnect: false,  // 网络恢复时不重新请求
  refreshInterval: 0,            // 轮询间隔(毫秒),0表示不轮询
  dedupingInterval: 2000,        // 2秒内的相同请求会被去重
});

这些配置项的名字很直白,基本看名字就懂。

场景1:实时数据(股票、在线人数)

const { data } = useSWR('/api/stock/AAPL', fetcher, {
  refreshInterval: 1000,  // 每秒轮询一次
  revalidateOnFocus: true // 切回页面立即更新
});

这种数据时效性要求高,恨不得时时刻刻都是最新的。轮询是最简单的方案(当然,更好的是用WebSocket,但那是另一个话题了)。

场景2:相对稳定数据(用户信息、文章列表)

const { data } = useSWR('/api/profile', fetcher, {
  revalidateOnFocus: false,     // 不要每次聚焦都刷新
  refreshInterval: 0,           // 不轮询
  dedupingInterval: 60000,      // 1分钟内不重复请求
});

用户资料一般不会频繁变化。切换tab不需要刷新,节省请求。但如果用户主动修改了资料,你还是可以手动触发更新(后面讲mutate)。

场景3:几乎静态的内容(文档、帮助页)

const { data } = useSWR('/api/docs', fetcher, {
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
  revalidateOnMount: false,     // 连挂载时都不验证
  revalidateIfStale: false,     // 就算数据是stale的也不验证
});

这种数据可能一个月都不会变。首次加载后,基本就是永久缓存了。除非用户手动刷新页面。

全局配置vs局部配置

如果整个应用的策略比较统一,可以用SWRConfig设置全局默认值:

import { SWRConfig } from 'swr';

function App() {
  return (
    <SWRConfig value={{
      refreshInterval: 3000,
      fetcher: (url) => fetch(url).then(r => r.json()),
      revalidateOnFocus: false,
    }}>
      <Dashboard />
    </SWRConfig>
  );
}

这样所有子组件的useSWR都会继承这些配置。当然,单个useSWR还可以override覆盖:

// 这个组件的revalidateOnFocus会是true,覆盖全局的false
const { data } = useSWR('/api/realtime', fetcher, {
  revalidateOnFocus: true
});

请求去重:节省你的API配额

有个很实用的功能——自动请求去重。假设你有三个组件同时挂载,都用useSWR('/api/user')获取用户数据。SWR不会发三次请求,而是只发一次,三个组件共享结果。

这个”去重窗口”默认是2秒(dedupingInterval: 2000)。也就是说,2秒内的相同请求会被合并。如果你的组件可能快速重新挂载(比如快速切换tab),可以把这个值调大一点。

说实话,这个功能救过我。之前有个列表页,用户快速滚动时会触发多次数据加载(我写的逻辑有点问题),结果同一个请求短时间内发了十几次。加上这个去重配置后,立刻就正常了。虽然治标不治本,但至少没把API配额用光(笑)。

条件fetching:依赖其他数据的请求

有时候你需要先获取A数据,再根据A的结果获取B。SWR有个小技巧——key传null就会暂停请求:

// 先获取用户信息
const { data: user } = useSWR('/api/user', fetcher);

// 等user加载完毕,再获取user的项目列表
const { data: projects } = useSWR(
  user ? `/api/projects?userId=${user.id}` : null,
  fetcher
);

user是undefined时,key是null,SWR不会发请求。等user加载完了,key变成有效值,SWR才会开始请求projects。串行依赖,就这么简单。

乐观更新:提升用户体验的秘密武器

缓存解决了”快”的问题,但还有个场景没覆盖到——用户操作。比如用户点了”点赞”,你是让他等API返回再显示红心?还是立即显示红心,后台偷偷发请求?

这就是乐观更新(Optimistic Update)的意义——假设操作会成功,立即更新UI,失败了再回滚。听起来有点冒险?但实际上大部分操作都会成功(除非网络问题或服务器挂了),用户体验提升是肉眼可见的。

对比一下传统方式和乐观更新

传统方式:

  1. 用户点击”点赞”
  2. 按钮变成loading状态(或者禁用)
  3. 等待500ms-2s(看网速)
  4. API返回成功,红心亮起
  5. 用户:“这网站有点慢啊”

乐观更新:

  1. 用户点击”点赞”
  2. 红心立即亮起(本地更新)
  3. 后台发送API请求
  4. (通常)成功,啥都不用做
  5. (偶尔)失败,红心恢复灰色,提示”操作失败”

后者的延迟是0ms。用户感觉?飞快。

mutate函数:手动控制缓存

SWR提供了mutate函数来手动更新缓存。最简单的用法:

import { mutate } from 'swr';

// 手动触发 /api/user 的重新验证
mutate('/api/user');

但乐观更新需要更多控制。来看一个完整的Todo应用示例:

import useSWR, { mutate } from 'swr';

function TodoList() {
  const { data: todos } = useSWR('/api/todos', fetcher);

  const addTodo = async (text) => {
    const newTodo = { id: Date.now(), text, completed: false };

    // 核心:乐观更新配置
    mutate(
      '/api/todos',
      async (currentTodos) => {
        // 1. 立即在UI显示新Todo(乐观)
        const optimisticData = [...currentTodos, newTodo];

        // 2. 后台发送真实请求
        const savedTodo = await fetch('/api/todos', {
          method: 'POST',
          body: JSON.stringify(newTodo)
        }).then(r => r.json());

        // 3. 用真实数据替换临时数据
        return [...currentTodos, savedTodo];
      },
      {
        optimisticData: [...todos, newTodo],  // 立即显示
        rollbackOnError: true,                // 失败时回滚
        revalidate: false,                    // 不需要额外验证
      }
    );
  };

  return (
    <div>
      {todos?.map(todo => <div key={todo.id}>{todo.text}</div>)}
      <button onClick={() => addTodo('New task')}>Add</button>
    </div>
  );
}

这段代码有点长,但逻辑很清晰:

  1. 用户点击”Add”
  2. 立即在列表里显示新Todo(optimisticData
  3. 后台发送POST请求
  4. 如果成功,用服务器返回的真实数据替换(可能有id、timestamp等字段)
  5. 如果失败,回滚到之前的状态(rollbackOnError: true

四大核心选项

mutate的options对象有四个关键配置:

1. optimisticData:立即更新的数据

optimisticData: [...todos, newTodo]  // 或者传函数

可以传固定值,也可以传函数。函数形式更灵活:

optimisticData: (currentTodos) => [...currentTodos, newTodo]

2. populateCache:是否用返回值更新缓存

默认是true。大多数情况你希望用API返回的数据更新缓存(因为服务器可能添加了id、createdAt等字段)。但如果API返回的不是完整数据,可以设为false。

3. revalidate:是否重新验证

通常设为false。因为你已经手动更新了数据,不需要SWR再发一次请求。除非你不信任自己的更新逻辑,想让SWR再确认一遍。

4. rollbackOnError:失败时是否回滚

超级重要。设为true的话,如果mutate函数抛出错误,SWR会自动回滚到更新前的数据。用户点了”点赞”,红心亮了,但API失败了?SWR自动让红心变回灰色。

还可以传函数做更细粒度控制:

rollbackOnError: (error) => {
  // 网络超时不回滚,其他错误回滚
  return error.name !== 'AbortError';
}

实战:删除Todo

删除操作也是乐观更新的经典场景:

const deleteTodo = async (id) => {
  mutate(
    '/api/todos',
    async (currentTodos) => {
      // 立即从列表中移除
      const optimistic = currentTodos.filter(t => t.id !== id);

      // 后台发送删除请求
      await fetch(`/api/todos/${id}`, { method: 'DELETE' });

      // 返回更新后的数据
      return optimistic;
    },
    {
      optimisticData: todos.filter(t => t.id !== id),
      rollbackOnError: true,
    }
  );
};

用户点击删除,Todo立即消失。如果服务器说”这条Todo不存在”或者请求失败了?Todo又会出现,并显示错误提示。

useSWRMutation:更优雅的写法

SWR 2.0引入了专门的useSWRMutation Hook,代码更简洁:

import useSWRMutation from 'swr/mutation';

async function updateUser(url, { arg }) {
  await fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  });
}

function Profile() {
  const { trigger, isMutating } = useSWRMutation('/api/user', updateUser);

  return (
    <button
      onClick={() => trigger({ name: 'John' })}
      disabled={isMutating}
    >
      Update Name
    </button>
  );
}

trigger就是用来触发mutation的,isMutating告诉你请求是否正在进行。比手动管理loading状态舒服太多。

什么时候该用乐观更新?

不是所有操作都适合乐观更新。经验法则:

适合乐观更新

  • 点赞/收藏(失败率低,可逆)
  • 添加/删除列表项(用户立即看到结果更重要)
  • 开关状态切换(像切换通知设置)
  • 表单输入保存(草稿功能)

不适合乐观更新

  • 支付操作(必须等确认)
  • 删除账户(不可逆,必须慎重)
  • 敏感数据修改(密码、权限)
  • 需要服务器计算的操作(比如”生成报告”,你不知道结果长啥样)

原则就是:操作失败率低、可逆、用户体验优先的场景,用乐观更新。关键业务逻辑、不可逆操作,老老实实等服务器返回。

说个我踩的坑。之前做一个评论功能,用了乐观更新。用户发评论,立即显示,体验很好。结果有天发现有用户在疯狂刷评论——因为他网络很慢,点了”发送”后没反应(其实已经发送了),他就连续点了十几次。每次点击都optimistic更新了,于是页面上出现了十几条相同的评论。虽然后台API有防重复逻辑,但前端UI还是乱了。

后来加了个isMutating判断,按钮在请求期间禁用,问题解决。教训就是:乐观更新很爽,但要考虑边界情况。

SWR vs React Query:如何选择?

聊了这么多SWR的好处,你可能会问:那React Query呢?网上搜”React数据获取”,React Query的文章也一大堆。到底选哪个?

坦白讲,这两个库都很优秀,2025年都还在积极维护。选错了不会死,但选对了能让开发更舒服。

体积对比:SWR更轻

  • SWR: 5.3KB (gzip后)
  • React Query (TanStack Query): 16.2KB

如果你在做一个对包体积特别敏感的项目(比如营销落地页、移动端H5),SWR的优势明显。但说实话,对于大多数项目,这10KB的差距没那么要命。

复杂度对比:SWR更简单

SWR的API设计极简,核心就一个useSWR Hook。5分钟能学会,10分钟能上手。

React Query功能更强大,但相应的学习曲线也更陡。你需要理解QueryClientuseQueryuseMutationqueryKeys、cache time vs stale time等概念。文档很长。

如果团队技术栈比较新(或者前端经验不多),SWR更友好。

功能对比:React Query更全面

React Query有几个SWR没有的功能:

  1. 官方DevTools:可视化查看所有query状态、缓存内容、refetch周期。调试体验超棒。SWR没有官方DevTools。

  2. 更细粒度的缓存控制:可以分别设置cache time(数据在内存中保留多久)和stale time(数据多久会变stale)。SWR相对简单,就是cache+revalidate。

  3. paginated/infinite queries:React Query有专门的useInfiniteQuery,SWR是useSWRInfinite。功能差不多,但React Query的API更成熟一些。

  4. mutation支持更强:React Query的useMutation设计更系统,有全局mutation state、retry策略、onSettled回调等。SWR的useSWRMutation是2.0才加的,功能相对简单。

社区和生态

React Query(现在叫TanStack Query)的社区更大,NPM周下载量也更多。你遇到问题,StackOverflow上找答案更容易。

SWR是Vercel出品,和Next.js是一家。官方文档对Next.js有专门的集成指南,如果你用Next.js,SWR是”亲儿子”。

我的选择建议

选SWR选React Query
项目相对简单,数据获取逻辑不复杂项目复杂,需要精细化控制缓存
包体积敏感(移动端)不在乎多10KB
用Next.js用其他框架(CRA、Vite等)
团队前端经验不多团队熟悉复杂库,喜欢丰富功能
想要快速上手愿意花时间学习完整生态

我自己的经验?小项目或MVP阶段,首选SWR,简单快速。项目大了、需求复杂了,可能会迁移到React Query。但说实话,大多数项目SWR完全够用。

还有一个点:两者迁移成本其实不高。核心概念都差不多(cache、revalidate、mutation),如果哪天真的需要React Query的高级功能,切换过去也不会太痛苦。不用过度纠结。

Next.js与SWR:天作之合

SWR和Next.js都是Vercel的产品,天生就配合得很好。如果你用Next.js,有几个特别方便的集成点。

App Router中使用SWR

Next.js 13+的App Router默认是服务器组件(Server Components)。SWR是客户端库,所以需要在Client Component中使用:

'use client';  // 必须标记为客户端组件

import useSWR from 'swr';

export default function Profile() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data?.name}</div>;
}

记得加'use client'。这是新手常忘的。

SSR + SWR:配合fallback data

Next.js的强项是服务器端渲染(SSR)。你可以在getStaticPropsgetServerSideProps里获取初始数据,然后传给SWR作为fallback:

// pages/profile.js
export async function getStaticProps() {
  const user = await fetch('https://api.example.com/user').then(r => r.json());

  return {
    props: {
      fallback: {
        '/api/user': user  // key对应SWR的key
      }
    },
    revalidate: 60  // ISR: 每60秒重新生成
  }
}

export default function Profile({ fallback }) {
  return (
    <SWRConfig value={{ fallback }}>
      <UserProfile />
    </SWRConfig>
  );
}

function UserProfile() {
  // 首次渲染会用fallback数据,无需loading
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data.name}</div>;
}

这样用户首次访问就能看到完整内容(SEO友好),同时SWR会在客户端接管后继续revalidate。两全其美。

预取(Prefetching)

对于重要的数据,可以在用户还没点击的时候就预先加载:

import { mutate } from 'swr';

function ArticleLink({ id }) {
  const prefetch = () => {
    mutate(`/api/article/${id}`, fetch(`/api/article/${id}`).then(r => r.json()));
  };

  return (
    <Link href={`/article/${id}`} onMouseEnter={prefetch}>
      Read more
    </Link>
  );
}

用户鼠标悬停在链接上时,就开始加载文章数据。等他点击时,数据可能已经在缓存里了,页面瞬间打开。

无限滚动:useSWRInfinite

做列表页最常见的需求——滚动加载更多:

import useSWRInfinite from 'swr/infinite';

function ArticleList() {
  const getKey = (pageIndex, previousPageData) => {
    if (previousPageData && !previousPageData.length) return null; // 没有更多了
    return `/api/articles?page=${pageIndex + 1}&limit=10`;
  };

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

  const articles = data ? data.flat() : [];
  const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');

  return (
    <div>
      {articles.map(article => (
        <div key={article.id}>{article.title}</div>
      ))}
      <button
        onClick={() => setSize(size + 1)}
        disabled={isLoadingMore}
      >
        {isLoadingMore ? 'Loading...' : 'Load More'}
      </button>
    </div>
  );
}

setSize(size + 1)就会加载下一页。SWR会缓存每一页的数据,用户往回滚也不需要重新加载。

性能优化建议

在Next.js中用SWR,有几个优化要注意:

  1. 合理设置revalidate频率

    • 静态内容(文档、帮助):关闭自动revalidate
    • 用户相关数据:保持默认的focus revalidate
    • 实时数据:用refreshInterval轮询
  2. API路由缓存

    • Next.js的API路由可以配合Cache-Control header
    • SWR会尊重HTTP缓存头
  3. 避免瀑布请求

    • 如果多个数据有依赖关系,考虑在服务器端合并请求
    • 或者用Next.js的并行数据获取

说个我的实际案例。之前做一个博客站,文章列表页用了SWR + ISR(Incremental Static Regeneration)。首屏是静态生成的HTML(秒开),用户看到内容的同时SWR在后台check有没有新文章,有的话自动更新列表。体验特别丝滑,而且SEO完美。

这就是Next.js + SWR的魅力——静态站点的性能 + 动态应用的实时性。

结论

写了这么多,回头总结一下。

SWR解决了React数据获取的三大痛点:代码冗余、缺乏缓存、多组件同步。核心策略”stale-while-revalidate”既快(立即显示缓存)又准(后台更新)。加上自动revalidation、请求去重、乐观更新这些功能,基本覆盖了90%的数据获取场景。

如果让我总结几个最佳实践:

  1. 根据数据特性调整缓存策略:实时数据轮询,稳定数据长缓存
  2. 对用户操作使用乐观更新:前提是失败率低、可逆
  3. 善用key做数据共享:不要为了共享数据引入状态管理
  4. Next.js项目优先选SWR:配合fallback data做SSR
  5. 项目简单选SWR,复杂选React Query:不要过度设计

最后说句实话:SWR不是银弹。它解决不了糟糕的API设计,也代替不了合理的架构。但如果你的后端API已经设计得不错,SWR能让前端代码优雅很多很多。

下一个项目,试试SWR?相信我,用完你会回来感谢我的。

SWR完整使用流程

从安装到配置、使用、优化缓存的完整步骤

⏱️ 预计耗时: 1 小时

  1. 1

    步骤1: 安装和基础使用

    安装:
    ```bash
    npm install swr
    ```

    基础使用:
    ```tsx
    'use client'
    import useSWR from 'swr'

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

    export function UserList() {
    const { data, error, isLoading } = useSWR('/api/users', fetcher)

    if (error) return <div>Failed to load</div>
    if (isLoading) return <div>Loading...</div>

    return <div>{data.map(user => <div key={user.id}>{user.name}</div>)}</div>
    }
    ```

    关键点:
    • 使用'use client'标记为客户端组件
    • fetcher函数处理数据获取
    • useSWR返回data、error、isLoading
  2. 2

    步骤2: 配置全局选项

    使用SWRConfig:
    ```tsx
    'use client'
    import { SWRConfig } from 'swr'

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

    export function Providers({ children }) {
    return (
    <SWRConfig
    value={{
    fetcher,
    revalidateOnFocus: true,
    revalidateOnReconnect: true,
    refreshInterval: 0,
    }}
    >
    {children}
    </SWRConfig>
    )
    }
    ```

    常用选项:
    • revalidateOnFocus:窗口聚焦时重新验证
    • revalidateOnReconnect:网络重连时重新验证
    • refreshInterval:定时刷新(0表示不刷新)
    • dedupingInterval:去重间隔(默认2000ms)

    关键点:在根组件配置,所有子组件共享配置
  3. 3

    步骤3: Next.js集成(SSR)

    配合fallback data:
    ```tsx
    // app/users/page.tsx(Server Component)
    import { getUsers } from '@/lib/users'

    export default async function UsersPage() {
    const initialUsers = await getUsers()

    return <UsersList initialUsers={initialUsers} />
    }

    // components/UsersList.tsx(Client Component)
    'use client'
    import useSWR from 'swr'

    export function UsersList({ initialUsers }) {
    const { data } = useSWR('/api/users', fetcher, {
    fallbackData: initialUsers // 使用SSR数据作为初始值
    })

    return <div>{data.map(...)}</div>
    }
    ```

    优势:
    • 首屏数据来自SSR,加载快
    • 后续更新用SWR,体验好
    • 结合SSR和客户端缓存的优势

    关键点:使用fallbackData而不是initialData(fallbackData不会触发重新验证)
  4. 4

    步骤4: 乐观更新

    使用mutate做乐观更新:
    ```tsx
    import { mutate } from 'swr'

    async function updateUser(id: string, name: string) {
    // 乐观更新:立即更新UI
    mutate(`/api/users/${id}`, { ...user, name }, false)

    // 发送请求
    await fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify({ name })
    })

    // 重新验证:确保数据正确
    mutate(`/api/users/${id}`)
    }
    ```

    关键点:
    • mutate的第三个参数false表示不重新验证
    • 请求成功后再次mutate重新验证
    • 用户体验更好(立即看到更新)

常见问题

SWR是什么?为什么需要它?
SWR是Vercel出品的React数据获取库,核心是stale-while-revalidate(陈旧-重新验证)。

传统数据获取痛点:
• 每个数据获取都要写useState、useEffect、loading、error处理
• 20行代码打底
• 多个组件需要同一份数据时要么状态提升要么各自请求

SWR优势:
• 自动缓存:相同key的请求自动共享缓存
• 自动重新验证:窗口聚焦、网络重连时自动刷新
• 自动去重:相同请求只发一次
• 自动错误重试:请求失败自动重试
• 一个Hook简化90%的数据获取代码

三行代码解决80%的问题:
```tsx
const { data, error, isLoading } = useSWR('/api/users', fetcher)
```
SWR和React Query有什么区别?
SWR:
• 简单轻量,学习成本低
• 适合大多数项目
• 功能相对简单
• Vercel出品,Next.js集成好

React Query:
• 功能强大,功能丰富
• 适合复杂场景
• 学习成本高
• 更灵活,但配置更复杂

选择建议:
• 项目简单 → SWR
• 项目复杂 → React Query
• 不确定 → 先用SWR,真不行再换

关键点:不要过度设计,项目简单选SWR,复杂选React Query。
SWR怎么在Next.js中使用?
配合fallback data做SSR:
```tsx
// app/users/page.tsx(Server Component)
export default async function UsersPage() {
const initialUsers = await getUsers()
return <UsersList initialUsers={initialUsers} />
}

// components/UsersList.tsx(Client Component)
'use client'
export function UsersList({ initialUsers }) {
const { data } = useSWR('/api/users', fetcher, {
fallbackData: initialUsers // 使用SSR数据
})
return <div>{data.map(...)}</div>
}
```

优势:
• 首屏数据来自SSR,加载快
• 后续更新用SWR,体验好
• 结合SSR和客户端缓存的优势

关键点:
• 使用fallbackData而不是initialData
• fallbackData不会触发重新验证
• 适合Next.js App Router
SWR的缓存策略是什么?
核心:stale-while-revalidate(陈旧-重新验证)

流程:
1. 首次请求:显示loading
2. 请求成功:显示数据并缓存
3. 再次请求:立即显示缓存数据(快)
4. 后台重新验证:悄悄更新数据(准)
5. 更新完成:悄悄换上新数据(无感)

关键特性:
• 自动缓存:相同key自动共享
• 自动去重:相同请求只发一次
• 自动重新验证:窗口聚焦、网络重连时刷新
• 自动错误重试:请求失败自动重试

配置选项:
• revalidateOnFocus:窗口聚焦时重新验证
• revalidateOnReconnect:网络重连时重新验证
• refreshInterval:定时刷新
• dedupingInterval:去重间隔

优势:用户体验好,数据总是最新的,但不会看到loading。
如何实现乐观更新?
使用mutate做乐观更新:
```tsx
import { mutate } from 'swr'

async function updateUser(id: string, name: string) {
// 乐观更新:立即更新UI
mutate(`/api/users/${id}`, { ...user, name }, false)

// 发送请求
await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify({ name })
})

// 重新验证:确保数据正确
mutate(`/api/users/${id}`)
}
```

关键点:
• mutate的第三个参数false表示不重新验证
• 请求成功后再次mutate重新验证
• 用户体验更好(立即看到更新)

注意:如果请求失败,需要回滚到原始数据。
SWR适合什么场景?
适合的场景:
• 需要频繁更新的数据(用户列表、通知等)
• 多个组件需要同一份数据
• 需要自动缓存和重新验证
• Next.js项目(集成好)

不适合的场景:
• 简单的静态数据(不需要缓存)
• 只需要一次请求的数据(不需要重新验证)
• 复杂的缓存需求(用React Query)

选择建议:
• 大多数项目 → SWR
• 复杂场景 → React Query
• 简单数据 → 直接用fetch

关键点:不要为了用SWR而用SWR,根据实际需求选择。

20 分钟阅读 · 发布于: 2025年12月19日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章