结合实践谈谈 Next.js 工程如何用 useSWR

1,053 阅读6分钟

Pasted image 20230703095612.png

业务中经常会遇到一个问题,某些接口的数据比较容易过期,需要频繁获取,同时这个数据需要被多个页面的组件消费。Next.js 13 提供的 fetch 方法只能在首屏渲染的时候获取数据,且 cache: 'no-store' 策略无法跨页面共享(即使数据没有过期,仍会发送请求)。常规解法是状态管理库 + 客户端定时轮询,但是需要手动处理很多逻辑,比如去除重复请求、超时处理、错误重试等,比较麻烦(本人之前造过很多类似的轮子)。

主动调研 useSWR 解服务端状态管理,具有以下特性:

1)传统状态管理库无法 data fetch,需要开发者自行获取数据、处理错误重试、数据过期问题,使用 useSWR ,组件将会不断地自动获得最新数据流,UI 也会一直保持快速响应

2)可以用服务端状态作为初始的 fallbackData

3)核心功能就是数据缓存,默认支持自动 revalidate、轮询逻辑、错误重试;

4)useSWR 可以实现 request deduping,去除重复请求,意味着可以在同一个页面多个组件同时用 useSWR,但是最终会合并为一次请求,这也是服务端状态管理的精髓;

5)如果数据过期,可以调用 useSWRConfigmutate 方法手动更新,同时广播给所有 useSWR 更新组建状态;

6)useSWR 除了可以在组件层面拿到 dataerrorisLoadingisValidating 等状态用于渲染(这里的 isLoading 表示目前暂无缓存,正在进行初次加载,isValidating 则表示已经有缓存,正在进行数据重新验证的加载),也可以监听 onSuccessonError 等事件回调用于在 JS 层面处理请求异常。

这里再补充一些使用小技巧。

当用户重新聚焦一个页面或在标签页之间切换时,SWR 会自动重新请求数据。这个功能非常实用,可以保持网站同步到最新数据。

重新请求对于在长时间位于后台的标签页,或休眠等情况下刷新数据非常有用。该特性默认是启用的,开发者可以通过 revalidateOnFocus 选项禁用它。

useSWR(key, fetcher, {
  revalidateIfStale: false,
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
});

// 相当于
useSWRImmutable(key, fetcher);

与 React Query 类似,SWR 不是请求库。useSWR 通过 fetcher 声明数据源,而提供给 fetcher 的只要是 fulfilled 或 rejected 的 Promise 即可,背后是啥请求库 SWR 并不关心。所以,可以用 axios、fetch、graphql 等请求库,因为他们都支持返回 Promise。但是需要注意,Next.js 工程无需引入第三方请求库(比如 Axios),由于 Next.js 内置了 fetch polyfill,可以直接用。

// 定义一个 baseUrl,这样就无需每个接口都重复写
const baseUrl = "https://xxx";

// 推荐将接口路径定义为枚举
export enum ReqPathEnum {
  API_USER = "/api/user",
}

export const fetcher = (url: ReqPathEnum) => fetch(baseUrl + url).then((res) => res.json());

一些公共配置,也可以通过 SWR 的全局配置,为所有的请求设置相同的策略。全局配置方式如下。一般我们会放在 App.tsx 中以保证包裹了所有的组件,然后在 value 中传入你的全局配置。

<SWRConfig value={options}>
  <Component/>
</SWRConfig>

如果我们想要在使用 hook 时为请求的响应值提供类型,只需要传入一个泛型就OK,如下例:

const { data } = useSWR<User>('/api/user', fetcher);

实际项目中推荐 hook 的方式进行二次封装:

export const useUser = () => {
  const { data, isLoading } = useSWR(`/api/user/userInfo`, fetcher)
  return { user: data, isLoading }
}

GET 请求如何传递参数:

function useList(params) {
  const { data, isLoading } = useSWR(['/api/list/listInfo', params], fetcher)
  return { List: data, isLoading }
}

需要注意的是,fetcher 函数会按原样接受参数,并且缓存 key 也将与整个参数数组相关联

可以把所有影响表格数据的参数都放在一个对象里,这样就可以直接改变对象的值来触发重新请求:

function List(){
  const [params, setParams] = useState({page: 1, pageSize: 10, filter:...});

  const { List , isLoading } = useList(params);

  const handleSearch = (values) => {
    setParams(values);
  };

  return (
    <div>
      <SearchForm onSearch={handleSearch} />
      <Table dataSource={List} loading={isLoading}/>
    </div>
  );
}

有时候我们需要首次加载不请求数据,只有用户点击按钮或者某个条件满足时才请求数据。由于 hooks 的限制,我们不能直接使用判断语句来控制请求。有两种解决方案:

  1. 设置 keynullswr 就不会请求数据了:
function useList(params) {
  const { data, isLoading } = useSWR(params ? ['/api/list/listInfo', params] : null, fetcher)
  return { List: data, isLoading }
}
  1. 使用 useSWRMutation,这个 hooks 只能手动触发,而不像 useSWR 那样会自动触发
import useSWRMutation from 'swr/mutation';

function Profile() {
  // 一个类似 useSWR + mutate 的 API,但是它不会自动发送请求
  const { trigger } = useSWRMutation('/api/user', updateUser, options?)

  const handleClick = () => {
    trigger();
  }

  return <button onClick={handleClick}>Update User</button>
}

注意这个 hook 不会与其他 useSWRMutation hook 共享状态。可以看出 useSWRMutation 比较适合用于 POST 请求,但 POST 请求显然直接调用请求库更简单,可以不用 useSWR

开启 keepPreviousData 选项时,请求数据,并在之后重新请求,保留之前的数据可以极大提升用户体验。这在用户连续操作的情况下进行数据请时对提升用户体验非常有用:

const { data, isLoading } = useSWR(`/search?q=${search}`, fetcher, {
  // 注意该选项默认为 false
  keepPreviousData: true,
});

有很多方法可以为 SWR 设置预请求数据。对于顶级请求,强烈推荐 rel="preload"

<link rel="preload" href="/api/data" as="fetch" crossorigin="anonymous">

SWR 提供预加载 API 以编程方式预取资源并将结果存储在缓存中。 preload 接受 key 和 fetcher 作为参数。 开发者甚至可以在 React 之外调用预加载。

import useSWR, { preload } from "swr";

// 在渲染之前调用
preload("/api/user", fetcher);
preload("/api/movies", fetcher);

const Page = () => {
  // 参考 CSR 最佳实践,渲染组件之前预加载数据
  // 可以解决潜在的瀑布问题
  const { data: user } = useSWR("/api/user", fetcher, { suspense: true });
  const { data: movies } = useSWR("/api/movies", fetcher, { suspense: true });
  return (
    <div>
      <User user={user} />
      <Movies movies={movies} />
    </div>
  );
};

啥时候会用到 preload,CSR 应用解瀑布流问题,可以在打包入口加上 preload,或者每个懒加载路由的页面组件加 preload(个人认为更好的解法是改用 React-Router v6 的 data loader 获取数据)。Next.js 项目也可以用,但是更好的解法是直接在 Node 环境获取数据

如果想在 SWR 缓存中预填充已经存在的数据(比如 SSR 渲染中非常常见的场景,服务端数据作为初始值),可以使用 fallbackData 选项,例如:

useSWR("/api/data", fetcher, { fallbackData: prefetchedData });

React Query 和 SWR 都可通过配置切换到 Suspense 模式。

import useSWR from 'swr';

function Profile () {
  const { data } = useSWR('/api/user', fetcher, { suspense: true })
  return <div>hello, {data.name}</div>
}

export default function Page() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Profile />
    </Suspense>
  )
}

参考:

github.com/vercel/swr/…

# 都什么时代还在发传统请求?来看看 SWR 如何用 React Hook 实现优雅请求

或许,你根本不需要全局状态管理