阅读 767
基于 ReactQuery 的服务端状态管理

基于 ReactQuery 的服务端状态管理

背景

在项目中,通常都需要跟服务端进行异步的数据交互,这包括 查询变更。 以一个简单的列表查询为例,我们通过 axios 去请求服务端的列表数据:

OK! 数据已经成功的取到了,也就是我们完成了跟服务端的一次查询交互了。现在我们来尝试更进一步,在 React 中可以通过实现一个 Hooks 把查询做的更优雅一点:

Perfect!? 并没有!

我们遗漏了非常重要的 请求状态 的处理,包括异常和请求进行中的情况,让我们继续完善 useListQuery:

以上,就是一个典型的请求处理的场景,为了实现它,我们写了近 30 行代码 ... 用来刷代码行数也是极其不错的 ... 🤣

那难道没有一种标准的请求处理模式吗?当然有!接下来我们进入正题,来看看 ReactQuery 的解决方案。

请求处理模式

初识 ReactQuery 的第一印象,通常都源于它提供的开箱即用的 QueryMutation 的 API.

哦,React 有一个请求库了

这就是 ReactQuery 能力的第一重境界 -- 请求处理

它通过 useQueryuseMutation 等 Hooks API, 提供了一系列标准的请求处理模式。

查询

那么首先来看看 ReactQuery 是怎么处理我们的列表请求的:

useQuery 通常包含两个参数:

  1. 一个能唯一标识这个请求的 Query key
  2. 一个真正执行请求并返回数据的异步方法

ReactQuery 的缓存策略是基于这个 key 来实现的。key 值除了字符串外,还可以是一个数组或者对象:

useQuery('list', ...)
useQuery(['list'], ...)

// 数组或对象作为 key 时通常都包含查询条件
useQuery(['list', 1], ...)
userQuery(['list', {
  page: 1
}])
useQuery({
  type: 'list',
  page: 1
})
复制代码

Query key 唯一的要求就是可以被序列化

而对于请求方法,useQuery 要求是一个 then-able 的函数即可,在我们日常使用情况中,通常指代的就是返回 Promise, 而 Promise 的返回值即请求的响应数据。

更多关于 userQuery 的用法可直接参考 userQuery API Reference

变更

useQuery 类似,ReactQuery 也提供了数据变更的 Hooks API:

useMutation 的参数通常包含一个真正执行请求的异步方法,返回值第一项为逻辑完备的 mutate 异步方法,在按钮点击后,可以通过调用 mutate 提交数据以及状态的处理。

更多关于 useMutation 的用法可直接参考 useMutation API Reference

ReactQuery 在请求处理上给我们提供了一个标准的处理方案,但是它的角色远不止请求库这么简单。在官方文档的 Overview 中作者就给了一个定位:

React Query is often described as the missing data-fetching library for React, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your React applications a breeze.

全局服务端状态管理

接下来,进入 ReactQuery 的第二重境界 -- 全局服务端状态管理

Global Server State Management

什么是 Server State

首先,我们需要知道什么是服务端状态。在无意识的行为中,我们通常都将所有的组件渲染所需要的数据都放在一起管理,比如放在 State 中或者通过 Redux 这类状态管理库来管理。

然而,我们再来斟酌一下我们的数据,是不是通常都有明显的来源特征:

  • 列表数据、详情数据等通过调接口由服务端提供的数据;
  • 选中状态、折叠状态这类由客户端来维护的状态;

基于数据的来源,我们就可以将组件渲染所需要的状态分为服务端状态客户端状态

ReactQuery 的状态管理

ReactQuery 就将我们所有的服务端状态维护在全局,并配合它的缓存策略来执行数据的存储和更新。借助于这样的特性,我们就可以将所有跟服务端进行交互的数据从类似于 Redux 这样的状态管理工具中剥离,而全部交给 ReactQuery 来管理。

ReactQuery 会在全局维护一个服务端状态树,根据 Query key 去查找状态树中是否有可用的数据,如果有则直接返回,否则则会发起请求,并将请求结果以 Query key 为主键存储到状态树中。

缓存

ReactQuery 的缓存策略使用了 stale-while-revalidate. 在 MDN 的 Cache Control 中对这个缓存策略的解释是:

客户端愿意接受陈旧的响应,同时在后台异步检查新的响应

在 ReactQuery 中的体现是,可以接受状态树中存储的 stale 状态数据, 并且会在缓存失效新的查询实例被构建refetch 等行为后执行更新状态。

关于 ReactQuery 缓存的处理过程,官方给了一个详细的示例

ReactQuery 还能解决这些问题

刷新列表状态

日常开发工作中经常需要处理在添加、删除或者编辑后刷新列表的数据。为了实现这个行为,我们通常需要将列表数据的状态抽取到列表和详情的父组件中去管理:

然而,在模式上,列表数据的只是 List 组件的数据源,应该收敛到 List 组件中去管理,而不应该放在父组件中。那如果这样的话,兄弟组件之间如何通信以达到更新列表数据的目的?似乎问题变得越来越复杂 ...

不怕!ReactQuery 来帮我们解这个问题~ 前面我们说过 ReactQuery 维护的是一个全局的状态树,那既然是全局的,问题不就简单了:

喂!详情页数据已经提交了,帮我更新下 Query Key 为 list 的数据吧!

ReactQuery 提供了 queryCache.invalidateQueries 可以直接指定某个 Query key 的缓存数据失效,这样 ReactQuery 就会在后台自动重新拉取最新的数据并更新到状态树中,这样列表组件中就渲染最新的数据了!完美!

usePaginatedQuery 和 useInfiniteQuery

除了基础的 useQuery 外,ReactQuery 还提供了 usePaginatedQueryuseInfiniteQuery, 分别来处理 分页无限加载 两个细分场景下的查询。

通过上述 API, 可以让我们在代码中免去维护类似于 分页 这样的客户端状态。

const [pagination, setPagination] = React.useState({
  currentPage: 1,
  pageSize: 10,
  totalSize: 0
});
复制代码

⚠️ 在使用 usePaginatedQuery 和 useInfiniteQuery 时,一定要保证 Query key 的唯一性,否则会造成难以预料的分页数据的混乱。

更多

除了本文中提到的这些基础能力外,ReactQuery 还提供了 PrefetchingSSROptimistic Updates 等高级特性。

另外,除了我们上面用到的 queryCache.invalidateQueries , queryCache 还包含很多 API 来方便我们做一些手动的的状态操作。

重要提示

  • 如果您看了本篇文章并开始使用 ReactQuery了,请一定不要忽视官方文档中的 Guides & Concepts: Important Defaults 中的 Important Defaults, 了解 ReactQuery 有那些默认的行为设置,这会帮助您构建更健壮的应用。
  • 本文编写时基于 ReactQuery@2 版本,目前 ReactQuery@3 版本已发布

我们是阿里巴巴 CCO 技术部团队,希望有更多的同学加入跟我们一起做更多有意义的事情。技术栈限 React,有 Typescript 经验者更佳,Base 南京或杭州。有意者联系:henry.lx@alibaba-inc.com

文章分类
前端
文章标签