内容作者为基因宝前端团队【一忱】
介绍
SWR是一个轻量的、负责请求数据、缓存数据的库。开发者可以使用hooks来发起请求,使用SWR的组件会自动获取数据。
基本写法如下:
async function fetcher(url, params) {
return await axios.get(url, params);
};
// 第一个参key,代表一个请求的唯一标识,如果key改变了SWR就会重新执行fetcher,更新缓存。
// 第二个参数fetcher,一个返回数据的请求函数。
// 默认每次调用SWR都要传入一个fetcher,也可以在SWR的configContext传一个通用fetcher。
// 第三个参数为config用来配置请求频率,是否重试,初始数据等。
const {
data,
error,
isValidating, //loading
mutate, // 函数,用来更改key对应的缓存
} = useSWR('/api/user', fetcher, {
shouldRetryOnError: true,
initialData: {
name: 'X',
},
onSuccess () {
},
});
features
这里只列举我在使用过程中认为很方便的特性,更详细的介绍参考SWR文档。
- 体积小,压缩后只有5kb,源码不到1000行。
- 对typescript友好。
- 使用SWR的组件会自动获取数据,如果参数未改变,useSWR被重新调用时(重新渲染或其他组件使用)就不会重新发请求,而是读取缓存。
- 如果组件未使用SWR的某个返回值(如某个组件没有使用isValidating)则不会重新渲染。
- 返回error和loading。
- 支持传入compare选项对比新旧请求数据避免不必要的渲染。
- 可利用SWR的缓存特性来替代mobx、redux、context状态管理方案。
- 支持结合axios这种请求库使用。
- 支持错误重试。
常见示例
- 传参
import {
useState,
useMemo,
} from 'react';
import useSWR from 'swr';
function Profile() {
const [selectedUser, setSelectedUser] = useState(null);
const params = useMemo(() => ({
id: selectedUser.id,
}), [
selectedUser,
]);
const {
data: userList,
} = useSWR('/api/users');
// 请求传参:默认key会作为fetcher的第一个参数,如果给fetcher传额外参数需要使用数组写法
// 上面提到过,第一个参数key的改变是SWR是否更新缓存的标识。你可能会想,这样下次渲染的时候key不是变了吗?
// 无需担心,SWR内部对数组类型key进行了hash处理,参数未变一定不会造成不必要的渲染。
// 条件请求:key为falsy,SWR不会调用fetcher
const {
data: profile,
} = useSWR(Number.isInteger(selectedUser?.id) ? ['/api/user', params] : null);
// 依赖请求:SWR通过key函数返回值来判断是否需要调用fetcher
// 实际上SWR对于依赖请求利用了错误捕获,如果key函数抛出错误就不会调用。
const {
data: projects,
} = useSWR(() => '/api/projects?uid=' + profile.id);
// 轮询
const {
data: projects,
} = useSWR('/api/polling', {
refreshInterval: 1000,
});
};
更新缓存(mutate)
import {
mutate, // 全局mutate
} from 'swr';
const {
data: profile,
mutate: updateProfile, // 已绑定key的mutate
} = useSWR('/api/user', {
onSuccess(newProfile) {
// 映射新请求的数据
updateProfile({
...newProfile,
age: 0,
});
}
});
// mutate: (
// data?: Data | Promise | MutatorCallback,
// shouldRevalidate?: boolean
// ) => Promise
// mutate的第二个参数shouldRevalidate,是否调用fetcher从远端获取数据
// 传true的目的是先更改本地数据,等待远端数据返回后替换
// 使用全局mutate重新请求某个key
const revalidateUser = useCallback(() => {
mutate('/api/user');
}, []);
// 基于已有数据更改
const updateUser = useCallback(() => {
updateProfile((currentProfile) => ({
...currentProfile,
name: '*',
}), false);
}, []);
// 异步函数
const updateUserAsync = () => {
updateProfile(async () => await Promise.resolve({
name: '*',
})), false);
};
实际使用
上面的示例是比较原始的写法,实际项目使用中遇到了几个问题
- 一个接口被多个组件同时使用,每次都要写useSWR('/api/xxx', config)吗?有点蠢,我应该把他们封装成hook。
- 由上一条延伸,一般项目的请求都会放在诸如services&api这种目录。
试想一下api目录下放着一些POST,DELETE等方法,给SWR调用的GET方法放到另一个目录下吗?很奇怪。 - 供SWR发起的请求被我封装成hooks了,能不能把修改数据的请求也封装成hooks,保证一致性?
说了这么多,根本问题就是目录结构。
参考了许多方案,挑选了一个我认为比较好的方案:
创建queries和mutations目录,queries放诸如useUserList用来查询数据,mutations放useUpdateUserList用来修改数据。
这样划分目录还是很清晰的,实际项目中SWR相关的hooks会很多,如果简单粗暴的将它们塞到hooks目录里难免会和其他通用的hooks混淆。
P.S.mutation实际上并不是SWR中的概念,而是react-query中的。
篇幅有限,详情可参考SWR开发团队成员SergioXalambrí基于react-query中的mutation概念写的use-mutation,当然你也可以写自己的useMutation。
管理全局状态
如果只是需要全局状态管理,完全可以用SWR替代mobx这种库。
P.S.事实上不使用我接下来介绍的globalState hook,SWR也承担了远程数据的管理,剩下的只有一些本地状态了。
function useGlobalState() {
const {
data,
mutate,
} = useSWR('globalState', {
initialData: initialStore,
});
return {
globalState: data,
mutateGlobalState: mutate,
};
};
function Content() {
const {
globalState,
} = useGlobalState();
return globalState.draft.content;
};
总结
SWR也是一个比较成熟稳定的库了,国外早就开始流行SWR或react-query,截止目前已经有17.1kstars。
个人觉得使用SWR提高了我的开发效率,并且简单易学。文章只是列举了一些常见用法和重要特性,SWR的源码有很多巧妙之处,大家更深入了解一下。
如果你有其他好的idea或文章有误,欢迎讨论与指正。
参考文章:
- How I Organize React Applications