基于SWR的应用与实践

内容作者为基因宝前端团队【一忱】

介绍

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);
};

实际使用

上面的示例是比较原始的写法,实际项目使用中遇到了几个问题

  1. 一个接口被多个组件同时使用,每次都要写useSWR('/api/xxx', config)吗?有点蠢,我应该把他们封装成hook。
  2. 由上一条延伸,一般项目的请求都会放在诸如services&api这种目录。
    试想一下api目录下放着一些POST,DELETE等方法,给SWR调用的GET方法放到另一个目录下吗?很奇怪。
  3. 供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

你可能感兴趣的:(基于SWR的应用与实践)