likes
comments
collection
share

React Query 实践

作者站长头像
站长
· 阅读数 44

本文正在参加「金石计划 . 瓜分6万现金大奖」

本文翻译自 TkDodoPractical React Query

GraphQLApollo Client 在 2018 年流行起来的时候 (特别是 Apollo Client),有很多它是否完全取代了 Redux 和和 Redux 是否已经死了的声音。

我清楚的记得我当时并不明白这是怎么回事,为什么一些数据请求库,会取代你的全局状态管理库,它们之间有什么关系?

我印象中像 Apollo 这样的 GraphQL client 只会为你像 Axios 那样为你获取数据,然后你仍然需要使用某种方式来让你的应用程序访问这些数据

我错得不能再错了。

客户端数据 vs. 服务端数据

Apollo 给你的不仅是获取数据的能力,它还为这些服务端数据提供了缓存,这意味着你可以在多个不同的组件中调用一个 useQuery,它只会请求一次,然后从缓存中返回

这听起来对我们来说似乎非常熟悉,但是是使用 Redux: 从服务端获取数据,并且可以在任何地方使用

似乎我们一直在把服务端数据当做客户端数据来使用,除了当你请求它时,你的应用并没有这些数据,只是展示了它的最新版本,真正的拥有者是服务端

对我来说,这引入了一个全新的数据思考范式,如果我们可以利用缓存来展示我们不曾拥有的服务端数据,那么真正的客户端状态就没剩下多少了,这让我明白了为什么很多人认为 Apollo 可以在很多情况下取代 Redux

React Query

我还没有使用过 GraphQL,我们有一个现有的 REST API,没有 over-fetching 的问题,很明显,我没有足够的痛点来进行转换,尤其是需要调整后端,这不是一个容易的事情

但是我仍然羡慕其获取数据的简单性,包括加载状态和错误状态的处理,要是 React 中也有类似的 REST API 就好了

进入 React Query

React Query 开源作者 Tanner Linsley 在 2019 年底开发的,它借鉴了 Apollo 中优秀的部分,并向其带入到了 REST API,它适用于任何返回 Promise 的函数,并且采用 stale-while-revalidate 缓存策略略,它有合适的默认值,使数据尽可能的保持新鲜,同时也尽可能的向用户提早展示数据,除此之外,它还非常灵活,可以在默认值不够用的情况下自定义各种设置

不过这篇文章不会是对 React Query 的介绍

官方的文档是很好的解释,你还可以看各种视频,如果你想熟悉它,有一个React Query Essentials课程,你可以参加。

我更多想关注的是一些超越文档的实用技巧,对于已经在使用它的同学来说可能会很有用,这些都是我在过去几个月里学到的,当时我不仅在工作中积极使用这个库,而且还参与了 React Query 社区,在 DiscordGitHub Discussions 中回答问题

默认值的解释

我相信 React Query 的默认值选择的很好,但是它们有时可能会让你措手不及,特别是在你刚使用它的时候

首先: React Query 不会在每次渲染的时候调用 queryFn 即使默认 staleTime 为 0,你的程序可能在任何时候因为各种原因重新渲染,所以每次渲染的时候请求数据是疯狂的

如果你看到了一个预料之外的 refetch,那可能是你刚刚聚焦了窗口,React Query 做了一个 refetchOnWindowFocus 的功能,这是一个很棒的生产功能,如果用户转到了一个不同的浏览器 Tab,然后又回到了你的应用程序,后台的重新请求会被自动触发,如果在用户离开的期间,服务端数据有什么变化,它们会被显示在用户的屏幕上,并且没有加载提示,如果重新请求的数据与缓存中的相同,那么你的组件将不会重新渲染

在开发期间,这可能会被频繁的触发,特别是在浏览器的 devtools 和你的应用程序之间的焦点切换也会触发它,

此外,cacheTimestaleTime 可能很容易混淆,所以我试着解释一下

  • StaleTime:查询的新鲜时间,只要查询是新鲜的,数据会始终从缓存中返回,并且不会发送网络请求,如果查询过期(默认是 0 即立即过期),你仍然会从缓存中获取数据,但是一个 refetch 可能在后台发生

  • CacheTime:从缓存中删除非活跃的查询之前的持续时间,默认是五分钟,一旦没有注册观察者,查询就会过渡到非活跃的状态,即当一个查询的组件都卸载时

使用 React Query DevTools

这会帮助你了解查询所处的状态,以及当前缓存中的数据,这会方便你的调试,此外如果想要识别后台 refetch,可以在浏览器的 devtools 中限制你的网络连接,因为开发服务器通常非常快

把 query key 当做依赖数组

我这里指得是 useEffect 钩子的依赖数组,我想你对它很熟悉

为什么它们是相似的

因为每当 query key 发生变化时,React Query 都会触发一个 refetch,因此,如果我们传递给 queryFn 一个 params 参数时,当 params 发生变化时我们大概率想要重新获取数据,所以与其利用复杂的手段来触发 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 变化了,因为我们也必须将 params 变量与 queryFn 同步,这与 useEffect 的依赖数组非常相似,我觉得我从来没有传递给 queryFn一个没有作为 query key的变量

一条新的缓存记录

因为 query key 是作为缓存的 key,所以当你第一次从 all 切换到 done的时候,你会获得一条新的缓存记录,这将导致一个硬加载的状态 (可能会展示一个 loading spinner),这是不理想的,所以在这些情况下你可以使用用 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
    },
  })

现在每次用户在切换状态时,如果缓存中没有数据,我们会先使用 todos all 中的数据进行预填充,一旦后台请求完成,用户会看到新的列表,注意在 v3 之前还需要设置 initialStale 属性,来实际触发后台请求

我觉得这是一个很棒的用户体验改进,只需几行代码

保持服务端和客户端的状态分离

这与我上个月写的一篇文章 puting-props-to-use-state 是相辅相成的,如果你使用 useQuery 获取数据,尽量不要把这些数据放进本地 state,主要原因是 React Query 会为你做所有的后台更新,但是"副本"不会

如果你想在数据获取后渲染你的表单,并为你的表单设置默认值,如下例子,这是没问题的,但是请确保设置 staleTimeInfinity 来避免不必要的后台 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)
  ...
}

但是当你显示的需要用户可以编辑时,这个理念更难贯彻一些,我准备了一个例子,它有很多优点

play

这个演示最重要的一点是我们没有把从 React Query 拿到的数据放在本地,这确保了我们看到的始终是最新的值,

enabled 选项非常强大

useQuery 有许多选项可以自定义它的行为,enable 选项可以非常强大,可以让你做很多酷的事情,下边是使用这个选项能实现的一些功能:

  • 从属查询

    只有第一个查询请求成功之后才进行第二个查询

  • 停启用查询

    由于 React Query 支持轮询数据(refetchInterval),但是如果 Modal 打开,我们想要它停止

  • 等待用户输出

    query key 中有一些筛选条件,但是如果用户没有输入就可以禁用它

  • 用户输入之后禁用一些查询

    例如我们有一个临时值,优先于服务器数据,参考上面的例子

不要把查询缓存当做本地的状态管理器

如果你更改 queryCache(queryClient.setQueryData),它应该仅限于乐观更新或者一个突变(useMutation)后从服务端收到的数据,每一次后台的 refetch 都有可能覆盖这些数据,所以本地状态需要使用其它方式存储

创建一个自定义的钩子

即使只为 useQuery 简单的包一层,也可以获得如下好处:

  • 可以将实际请求数据的 queryFn 和 UI 分离,但是和 useQuery 放在同一个地方

  • 可以将 query key 和它们的用法放在同一个文件里(包裹潜在的类型定义)

  • 如果需要增加或者调整一些数据转换,可以在一个文件夹内完成

你已经在上边的例子 (todos) 看到了这种用法

本文正在参加「金石计划 . 瓜分6万现金大奖」

转载自:https://juejin.cn/post/7164602747361165349
评论
请登录