业务中经常会遇到一个问题,某些接口的数据比较容易过期,需要频繁获取,同时这个数据需要被多个页面的组件消费。Next.js 13 提供的 fetch
方法只能在首屏渲染的时候获取数据,且 cache: 'no-store'
策略无法跨页面共享(即使数据没有过期,仍会发送请求)。常规解法是状态管理库 + 客户端定时轮询,但是需要手动处理很多逻辑,比如去除重复请求、超时处理、错误重试等,比较麻烦(本人之前造过很多类似的轮子)。
主动调研 useSWR
解服务端状态管理,具有以下特性:
1)传统状态管理库无法 data fetch,需要开发者自行获取数据、处理错误重试、数据过期问题,使用 useSWR
,组件将会不断地、自动获得最新数据流,UI 也会一直保持快速响应;
2)可以用服务端状态作为初始的 fallbackData
;
3)核心功能就是数据缓存,默认支持自动 revalidate、轮询逻辑、错误重试;
4)useSWR
可以实现 request deduping,去除重复请求,意味着可以在同一个页面多个组件同时用 useSWR
,但是最终会合并为一次请求,这也是服务端状态管理的精髓;
5)如果数据过期,可以调用 useSWRConfig
的 mutate
方法手动更新,同时广播给所有 useSWR
更新组建状态;
6)useSWR
除了可以在组件层面拿到 data
、error
、isLoading
、isValidating
等状态用于渲染(这里的 isLoading
表示目前暂无缓存,正在进行初次加载,isValidating
则表示已经有缓存,正在进行数据重新验证的加载),也可以监听 onSuccess
、onError
等事件回调用于在 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 的限制,我们不能直接使用判断语句来控制请求。有两种解决方案:
- 设置
key
为null
,swr
就不会请求数据了:
function useList(params) {
const { data, isLoading } = useSWR(params ? ['/api/list/listInfo', params] : null, fetcher)
return { List: data, isLoading }
}
- 使用
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>
)
}
参考: