ReactQuery系列React Query 实践示例详解

目录
  • 引言
  • 客户端状态 vs 服务端状态
  • React Query
    • 关于默认行为的解释
  • 使用React Query DevTools
    • 把query key理解成一个依赖列表
  • 一个新的缓存入口
  • 把服务端状态和客户端状态分开
  • enabled属性是很强大的
  • 创建自定义hook

引言

当2018年GraphQL特别是Apolllo Client开始流行之后,很多人开始认为它将替代Redux,关于Redux是否已经落伍的问题经常被问到。

我很清晰地记得我当时对这些观点的不理解。为什么一些数据请求的库会替代全局状态管理库呢?这两者有什么关联呢?

曾经我认为像Apollo这样的Graphql客户端只能用来请求数据,就像axios一样,你仍然需要一些方式来让请求的数据可以被应用程序访问到。

我发现我大错特错。

客户端状态 vs 服务端状态

Apollo提供的不仅仅是描述所需数据同时获取数据的能力,它同时提供了针对这些服务端数据的缓存能力。这意味着你可以在多个组件中使用相同的useQueryhook,它只会触发一次数据请求并且按照请求的先后顺序返回缓存中的数据。

这看起来跟我们(包括很多除了我们以外的团队)在一些场景使用redux的目的很相似:从服务器获取数据,然后让这部分数据可以在所有地方可以被访问到。

所以似乎我们经常将这些服务端数据当成客户端状态来看待,除了这些服务端数据(比如:一个文章列表,你需要显示的某个用户的详细信息,...),你的应用并不真正拥有它。我们只是借用了最新版本的一份数据然后展示给用户。服务端才真正拥有这部分数据。

对于我来说,这给了我一个如何看待数据的新的思路。如果我们能利用缓存来显示我们不拥有的那部分数据,那么剩下的应用需要处理的真正的客户端状态将大大减少。这使我理解了为什么很多人认为Apollo可以在很多场景替代redux。

React Query

我一直没有机会使用GraphQL。我们有现成的REST API,并没有遇到冗余请求的问题,目前完全沟通。并没有足够的理由让我们转换到GraphQL,特别是你还需要让后端服务配合进行改动。
但是我还是羡慕GraphQL带来的前端数据请求(包括loading和错误态的处理)的简洁。如果React生态中有相似的针对REST API的方案就好了。

让我们来看看React Query吧。

Tanner Linsley在2019年开发的React Query使得在REST API中也可以使用到Apollo所带来的好处。他支持任何返回Promise的函数并且使用了stale-while-revalidate缓存策略。库本身有一些默认行为可以尽可能保证数据的实时性,同时尽可能快的将数据返回给用户,让人们感觉近乎实时的体验以提供优秀的用户体验。在这之上,他同时提供了灵活的自定义能力来满足各种场景。

这篇文章并不会对React Query进行详细的介绍。

我认为官方文档已经对使用和概念进行了很好的介绍,同时也有很多关于这方面的视频,并且Tanner开了一门课程可以让你熟悉这个库。

我将会更多的关注在官方文档之外的一些实践上的介绍,当你已经使用这个库一段时间之后,也许这些介绍对你会有所帮助。这其中有一些我过去几个月在深度使用React Query以及从React Query社区中总结出的经验。

关于默认行为的解释

我相信React Query的默认行为是经过深思熟虑的,但是他们有时会让你措手不及,特别是刚开始使用的时候。

首先,React Query并不会在每次render的时候都执行queryFn,即使默认的staleTime是0。你的应用在任何时候可能会因为各种原因重新render,所以如果每次都fetch是疯狂的!
如果你看到了一个你不希望的refetch,这很可能是因为你刚聚焦了当前窗口同时React Query执行了refetchOnWindowFocus,这在生产环境是一个很棒的特性:如果用户在不同的浏览器tab之间切换,然后回到了你的应用,一个后台的refetch会被自动触发,如果在同一个时间服务端数据发生了变更,那屏幕上的数据会被更新。所有这些会在看不到loading态的情况下发生,如果数据和缓存中的数据对比没有变化的话,你的组件不会进行重新render。

在开发阶段,这个现象会出现得更加频繁,特别是当你在浏览器DevTools和你的应用之间切换的时候。

其次,cacheTimestaleTime的区别似乎经常让人感到困惑,所以让我来说明一下:

  • StaleTime:一个查询变成失效之前的时长。如果查询是有效的,那么查询就会一直使用缓存中的数据,不会进行网络请求。如果查询是处于失效状态(默认情况下查询会立即失效),首先仍然会从缓存中获取数据,但是同时后台在满足一定条件的情况下会发起一次查询请求。
  • CacheTime:查询从变成非激活态到从缓存中移除持续的时长。默认是五分钟,当没有注册的观察者的时候,查询会变成非激活态,所以如果所有使用了某个查询的组件都销毁的时候,这个查询就变成了非激活态。
    大多数情况下,如果你要改变这两个设置其中的某一个的话,大部分情况下应该修改staleTime。我很少会需要修改cacheTime。在文档里面也有一个关于这个的解释

使用React Query DevTools

DevTools会帮助你更好的理解查询中的状态。它会告诉你当前缓存中的数据是什么,所以你可以更方便的进行调试。除了这些,我发现在DevTools中可以模拟你的网络环境来更直观的看到后台refetch,因为本地服务一般都很快。

把query key理解成一个依赖列表

我这里所说的依赖列表是类比useEffect中说到的依赖列表,我假设你已经对useEffect已经比较熟悉了。

为什么这两者会是相似的呢?

因为React Query会触发refetch当query key发生变化。所以当我们给queryFn传了一个变量的时候,大部分情况下我们都是希望当这个变量发生变化的时候可以请求数据。相比于通过复杂的代码逻辑来手动触发一个refetch,我们可以利用query key:

type State = 'all' | 'open' | 'done'
type Todo = {
  id: number
  state: State
}
type Todos = ReadonlyArray<Todo>
const fetchTodos = async (state: State): Promise<Todos> => {
  const response = await axios.get(`todos/${state}`)
  return response.data
}
export const useTodosQuery = (state: State) =>
  useQuery(['todos', state], () => fetchTodos(state))

这里,想象我们的UI显示了一个带有过滤器的todo列表。我们会有一些本地状态来存储过滤器的数据,当用户改变了过滤条件之后,我们会更新本地的状态,然后React Query会自动触发一个refetch,因为query key发生了变化。我们最终实现了过滤状态和查询函数的同步,这与useEffect中的依赖列表很相似。我从来没有没有出现过给queryFn传了一个变量,但是这个变量不是queryKey的一部分的情况。

一个新的缓存入口

因为query key被用作缓存的key,所以当你把状态从all改成done的时候,你会得到一个新的缓存入口,当你第一次切换过滤状态的时候,会导致一个强制的loading状态(很可能会限制一个loading动画)。这当然不是最理想的,所以你可以使用keepPreviousData来处理这种情况,或者你可以使用initialData来为新的缓存入口预填充数据。上面那个例子可以很完美的解释这个情况,因为我们可以做一些客户端的数据预过滤:

type State = 'all' | 'open' | 'done'
type Todo = {
  id: number
  state: State
}
type Todos = ReadonlyArray<Todo>
const fetchTodos = async (state: State): Promise<Todos> => {
  const response = await axios.get(`todos/${state}`)
  return response.data
}
export const useTodosQuery = (state: State) =>
  useQuery(['todos', state], () => fetchTodos(state), {
    initialData: () => {
      const allTodos = queryClient.getQueryData<Todos>(['todos', 'all'])
      const filteredData =
        allTodos?.filter((todo) => todo.state === state) ?? []
      return filteredData.length > 0 ? filteredData : undefined
    },
  })

现在,每次用户切换过滤条件的时候,如果我们没有数据,我们会尝试用'all todos'缓存中的数据来预填充。我们可以马上就显示'done'的todo给用户,他们可以在后台fetch结束之后看到更新之后的列表。注意v3版本中,你需要设置initialStale属性来触发一个后台fetch。
我认为这简单的几行代码可以给你带来很好的用户体验的提升。

把服务端状态和客户端状态分开

这个观点和我上个月写的文档一样:如果你从useQuery中拿到了数据,不要把这部分数据放到本地状态中。主要的原因是这样会使得React Query所有后台更新失效,因为复制出来的本地状态不会自动更新。
如果你希望获取一些默认数据来设置一个表单的默认值,然后使用数据来渲染表单,那是可以的。后台更新并不会因为表单已经初始化就忽略之后更新的数据。所以如果你想打到这个目的,确保通过设置staleTime来避免触发不必要的后台refetch:

const App = () => {
  const { data } = useQuery('key', queryFn, { staleTime: Infinity })
  return data ? <MyForm initialData={data} /> : null
}
const MyForm = ({ initialData} ) => {
  const [data, setData] = React.useState(initialData)
  ...
}

enabled属性是很强大的

useQuery hook有很多属性可以用来自定义他的行为,enabled属性是很强大的一个,它可以让你做很多有意思的事情。下面是一些我们可以利用它来实现的功能:

在一个查询中获取数据,然后第二个查询只有当我们成功的从上一个查询中获取数据的时候才会触发

  • 开启/关闭查询

假设我们有一个定时查询,通过refetchInterval来实现,但是当一个弹窗打开的时候我们可以暂停这个查询,避免弹窗后面的内容发生变更。

  • 等待用户输入

比如我们有一些过滤条件作为query key,但是当用户还没进行过滤操作的时候可以不进行查询。

不要把queryCache当成本地状态管理器

如果你要修改queryCache,它应该只发生在乐观更新或者在变更之后拿到后台返回的新数据的时候。记住任何一个后台refetch都会覆盖这些数据,所以可以使用其他本地状态管理库

创建自定义hook

即使你只是封装一个useQuery调用,创建一个自定义hook通常情况下也是值得的,因为:

  • 你可以把真实的数据获取逻辑和UI分离,当时把它和useQuery调用封装在一起
  • 你可以把对于某个query key的使用都放在同一个文件里面
  • 如果你需要修改一些设置或者增加一些数据转换逻辑,你可以在一个地方进行
    在上面的todo例子里面已经有一些使用场景

我希望这些实践经验可以帮助你熟悉React Query,更多关于React Query 实践的资料请关注我们其它相关文章!

(0)

相关推荐

  • React 中state与props更新深入解析

    目录 正文 组件的 updater 处理 ClickCounter Fiber 的 update beginWork Reconciling children for the ClickCounter Fiber 处理 Span Fiber 的 update Reconciling children for the span fiber Effects list commit 阶段 应用 effects DOM updates 调用 Post-mutation 生命周期 正文 在这篇文章中,我使

  • ReactQuery系列之数据转换示例详解

    目录 引言 数据转换 后端 查询函数中 render函数中 使用select配置 引言 欢迎来到“关于react-query我不得不说的一些事情”的第二章节.随着我越来越深入这个库以及他的社区,我发现一些人们经常会问到的问题.最开始,我计划在一篇超长的文章里面把这些都讲清楚,最终我还是决定将他们拆分成一些有意义的主题.今天第一个主题是一个很普遍但是很重要的事情:数据转换. 数据转换 我们不得不面对这个问题-大部分的人并没有使用GraphQL.如果你使用了,那么恭喜你,因为你可以请求到你期望的数据

  • React setState是异步还是同步原理解析

    目录 setState异步更新 那么为什么setState设计为异步呢? 如何获取异步的结果 setState一定是异步的吗? setState异步更新 开发中当组件中的状态发生了变化,页面并不会重新渲染.我们必须要通过setState来告知React数据已经发生了变化,重新渲染页面. 先来看下面的例子: constructor() { super(); this.state = { message: "Hello World", }; } changeText() { this.se

  • ReactQuery系列React Query 实践示例详解

    目录 引言 客户端状态 vs 服务端状态 React Query 关于默认行为的解释 使用React Query DevTools 把query key理解成一个依赖列表 一个新的缓存入口 把服务端状态和客户端状态分开 enabled属性是很强大的 创建自定义hook 引言 当2018年GraphQL特别是Apolllo Client开始流行之后,很多人开始认为它将替代Redux,关于Redux是否已经落伍的问题经常被问到. 我很清晰地记得我当时对这些观点的不理解.为什么一些数据请求的库会替代全

  • react后台系统最佳实践示例详解

    目录 一.中后台系统的技术栈选型 1. 要做什么 2. 要求 3. 技术栈怎么选 二.hooks时代状态管理库的选型 context redux recoil zustand MobX 三.hooks的使用问题与解决方案 总结 一.中后台系统的技术栈选型 本文主要讲三块内容:中后台系统的技术栈选型.hooks时代状态管理库的选型以及hooks的使用问题与解决方案. 1. 要做什么 我们的目标是搭建一个适用于公司内部中后台系统的前端项目最佳实践. 2. 要求 由于业务需求比较多,一名开发人员需要负

  • 微服务架构之服务注册与发现实践示例详解

    目录 1 服务注册中心 4种注册中心技术对比 2 Spring Cloud 框架下实现 2.1 Spring Cloud Eureka 2.1.1 创建注册中心 2.1.2 创建客户端 2.2 Spring Cloud Consul 2.2.1 Consul 的优势 2.2.2 Consul的特性 2.2.3 安装Consul注册中心 2.2.4 创建服务提供者 3 总结 微服务系列前篇 详解微服务架构及其演进史 微服务全景架构全面瓦解 微服务架构拆分策略详解 微服务架构之服务注册与发现功能详解

  • React 路由使用示例详解

    目录 Router 简单路由 嵌套路由 未匹配路由 路由传参数 索引路由 活动链接 搜索参数 自定义行为 useNavigate 参考资料 Router react-router-dom是一个处理页面跳转的三方库,在使用之前需要先安装到我们的项目中: # npm npm install react-router-dom@6 #yarn yarn add react-router-dom@6 简单路由 使用路由时需要为组件指定一个路由的path,最终会以path为基础,进行页面的跳转.具体使用先看

  • TDesign在vitest的实践示例详解

    目录 起源 痛点与现状 vitest 迁移 配置文件改造 开发环境 集成测试 ssr 环境 csr 环境 配置文件 兼容性 结果 CI测试速度提升 更清爽的日志信息 起源 在 tdesign-vue-next 的 CI 流程中,单元测试模块的执行效率太低,每次在单元测试这个环节都需要花费 6m 以上.加上依赖按照,lint 检查等环节,需要花费 8m 以上. 加上之前在单元测试这一块只是简单的处理了一下,对开发者提交的组件也没有相应的要求,只是让它能跑起来就好.另一方面单元测试目前是 TD 发布

  • react使用useImperativeHandle示例详解

    目录 1.前言 2.useImperativeHandle初探 3.获取元素的几种方式 3.1 useRef:获取组件内部元素 3.2 forwardRef:父组件获取子组件内部的一个元素 3.3 useImperativeHandle:父组件可以获取/操作儿子组件多个元素 1.前言 相比大家看到useImperativeHandle会感到十分陌生,但部分开源代码经常会出现它的身影,网上查阅的资料也是含糊不清.经过一翻资料查询,终于摸清了一点,现在分享给各位爷. 2.useImperativeH

  • React Redux应用示例详解

    目录 一 React-Redux的应用 1.学习文档 2.Redux的需求 3.什么是Redux 4.什么情况下需要使用redux 二.最新React-Redux 的流程 安装Redux Toolkit 创建一个 React Redux 应用 基础示例 Redux Toolkit 示例 三.使用教程 安装Redux Toolkit和React-Redux​ 创建 Redux Store​ 为React提供Redux Store​ 创建Redux State Slice​ 将Slice Reduc

  • Gradle 依赖切换源码实践示例详解

    目录 引言 1.一般的修改办法 2.通过 Gradle 脚本动态修改依赖 2.1 配置文件和工作流程抽象 2.2 为项目动态添加子工程 2.3 使用子工程替换依赖 2.4 注意事项 总结 引言 最近,因为开发的时候经改动依赖的库,所以,我想对 Gradle 脚本做一个调整,用来动态地将依赖替换为源码.这里以 android-mvvm-and-architecture 这个工程为例.该工程以依赖的形式引用了我的另一个工程 AndroidUtils.在之前,当我需要对 AndroidUtils 这个

  • React Hook用法示例详解(6个常见hook)

    1.useState:让函数式组件拥有状态 用法示例: // 计数器 import { useState } from 'react' const Test = () => { const [count, setCount] = useState(0); return ( <> <h1>点击了{count}次</h1> <button onClick={() => setCount(count + 1)}>+1</button> &l

随机推荐