7

React Query的实战指南

 2 years ago
source link: https://www.ttalk.im/2021/12/practical-react-query.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

介绍React Query这个非常实用的数据获取和管理库,以及它在应用中的实用技巧

原文地址 Practical React Query

在2018年,当GraphQL尤其是它的实现Apollo变得非常流行时,很多人在忙着完全使用它代替Redux,并且Redux是不是完蛋了的这个问题也经常会被问到。

我清楚地记得我当时完全搞不明白这是怎么回事。为什么某些数据获取库会取代我们的全局状态管理器?它们之间有什么关系?

我的印象是像Apollo这样的GraphQL客户端只会为我们获取数据,类似于axios用于REST请求一样,而且我们仍然需要某种方式使我们的应用程序可以访问该数据。

但是我大错特错了。

客户端状态 VS 服务端状态

Apollo为我们提供的不仅仅是描述我们想要的数据和获取该数据的能力,它还为服务器数据提供了缓存。 这意味着我们可以在多个组件中使用相同的useQuery hook,它只会获取一次数据,然后从缓存中返回它。 这听起来非常熟悉我们,可能还有许多其他团队,主要使用redux的目的是:从服务器获取数据并使其随处可用。

因此,我们似乎一直像对待任何其它客户端状态一样对待此服务器状态。除了我们的应用程序不拥服务器状态(假设:我们获取的文章列表,我饿吗想要显示的用户的详细信息。。。)。 我们只是用它来为用户在屏幕上显示它的最新版本。拥有这些数据的是服务器。

对我来说,这让我对数据的模式思维模式发生了转变。如果我们可以利用缓存来显示我们不拥有的数据,那么整个应用中真正需要使用的的客户端状态就不会剩下多少了。 这让我明白为什么很多人认为Apollo可以在很多情况下取代redux。

React Query

我从来没有机会使用GraphQL。我们现有的REST API,并没有真正遇到过度获取的问题,并且它工作的很好。显然,我们没有足够的痛点来让我们去使用GraphQL,特别是考虑到我们还必须适配后端,这问题就不那么简单了。

然而,我仍然羡慕前端数据获取的简单性,包括加载和错误状态的处理。 如果React访问REST API的工具中中有类似的东西就好了。。。

使用React Query

它是由Tanner Linsley在2019年开发并开源的,它将Apollo中的优点带入了REST世界。它适用于任何返回Promise并采用stale-while-revalidate缓存策略的函数。该库在默认设置下,尝试使我们的数据尽可能新,同时尽可能早地向用户显示数据,使其有时感觉近乎即时,从而提供出色的用户体验。 最重要的是,它也非常灵活,当默认值无法满足我们的需求时,我们可以自定义各种设置。

不过,本文不会介绍React Query。

我认为React Query的文档非常适合解释概念和作为指南,你也可以观看来自各种演讲的视频,如果你想熟悉该库,可以学习Tanner的React Query Essentials 课程

我想更多地关注一些超出文档范围的实用技巧,当你已经在使用该库时,这些技巧可能会很有用。这些是我在过去几个月中学到的东西,当时我不仅在工作中积极使用该库,而且还参与了 React Query社区,在Discord和GitHub讨论中回答问题。

对默认值的解读

我相信React Query的默认值是非常好的选择,但它们有时会让你不明所以,尤其是在刚开始使用React Query的时候。 首先:默认情况下,即便老化时间已经到0了,React Query也不会在每次重渲染的时候调用queryFn。我们的应用可能会因为任何原因,随时进行重渲染,因此每次都获取数据是不合理的。

总是为重新渲染编写代码,而且这会很多。 我喜欢称之为按需渲染。 — Tanner Linsley

如果我们看到不期望的重新获取,很可能是因为我们只是聚焦了窗口,而React Query正在执行 refetchOnWindowFocus,这对正式版本来说是一个很棒的功能:如果用户转到不同的浏览器选项卡,然后又回来了到我们的应用程序,这将自动触发后台重新获取,如果在此期间服务器上的某些内容发生更改,则屏幕上的数据将更新。 所有这些都发生在没有显示加载进度条的情况下,如果数据与我们当前在缓存中的数据相同,我们的组件将不会重新渲染。

在开发过程中,这可能会更频繁地触发,特别是因为浏览器的DevTools和我们的应用程序之间的焦点切换也会导致获取,因此请注意这一点。

其次,缓存时间(cacheTime)和老化时间(staleTime)似乎非常容易混淆,所以让我试着澄清一下:

  • 老化时间:查询从全新变为陈旧所需要的时间。只要查询是全新的,数据将始终只从缓存中读取—这不会发出网络请求!如果查询过时(默认情况下是:立即),我们仍然会从缓存中获取数据,但在某些情况下可能会发生后台重新获取数据的情况。
  • 缓存时间:从缓存中删除非活动查询之前所需要的时间。 这默认为5分钟。一旦没有观察者了,查询就会转换到非活动状态,因为使用该查询的所有组件都已卸载了。

大多数情况下,如果我们想改变这些设置中的一个,那必须是老化时间(staleTime)。我们很少需要去更改缓存时间(cacheTime)。文档中有一个非常好的例子给为什么这么做做出了解释。

使用React Query的DevTools

这将极大地帮助我们了解查询所处的状态。DevTools还会告诉我们当前缓存中的数据,因此我们可以更轻松地进行调试。 除此之外,我发现如果我们想更好地识别后台重新获取,就需要在浏览器DevTools中限制我们的网络连接,因为开发服务器通常非常快。

将查询键key视为依赖数组

我在这里指的是我们所熟悉的useEffect hook的依赖数组。

为什么它们两个非常相似呢?

因为每当查询key发生变更时,React Query都会触发重新获取。 因此,当我们将可变参数传递给queryFn时,我们几乎总是希望在该值发生改变时获取数据。 我们可以使用查询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显示了一个待办事项列表以及一个过滤器选项。 我们会有一些本地状态来存储过滤条件,一旦用户改变了他们的选择,我们就会更新那个本地状态,React Query会自动为我们触发重新获取,因为查询key发生了变化。 因此,我们将用户的过滤器条件与查询函数保持同步,这与useEffect依赖数组表示的内容非常相似。我不认为我会将不属于queryKey的变量传递给queryFn。

一个新的缓存项

因为查询key用作缓存的key,当我们从“all”切换到“done”时,我们将获得一个新的缓存条目,当我们第一进行这个切换时,这将会有一个漫长的加载状态(可能显示加载进度条)。这很不理想,因此我们可以在这些情况下使用keepPreviousData选项,或者,如果可能,使用初始数据预填充新创建的缓存条目。 上面的例子非常适合这个,因为我们可以对我们的待办事项进行一些客户端预过滤:

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 = queryCache.getQuery<Todos>(['todos', 'all'])
      const filteredData =
        allTodos?.filter((todo) => todo.state === state) ?? []
      return filteredData.length > 0 ? filteredData : undefined
    },
  })

现在,每次用户在状态之间切换时,如果我们还没有数据,我们会尝试用“所有待办事项”缓存中的数据预先填充它。我们可以立即向用户显示“已完成”的待办事项,一旦后台获取完成,他们仍将看到更新的列表。 请注意,在v3之前,我们还需要设置initialStale属性以实际触发后台获取。

我认为这对于几行代码来说是一个很好的用户体验改进。

保持服务器和客户端状态分开

这于我在上个月所写的文章Putting props to useState有一定的关联:如果我们从useQuery中获取了数据,请尽量不要将该数据写入本地状态中。主要原因是,我们这样做会隐式的无法从React Query的后台更新中获得新的数据,因为状态中的副本是不会随之更新。

如果这是我们所想要的,那么这很好,例如,获取表单的默认值,并将获得数据呈现到我们的表单上。后台更新不太可能产生新的东西,并且我们的表单已经完成了初始化。因此,我们确实是有意为之时,请确保不要通过设置老化时间来触发不必要的后台获取:

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

}

const MyForm = ({ initialData} ) => {
  const [data, setData] = React.useState(initialData)
  ...
}

当我们显示一些允许用户编辑的数据时,这个概念会有点难以遵循,但它很有很多有点。因此我准备了一个codesandbox:

这个演示最重要部分是我们从不将从React Query获得的值放入本地状态。 这确保我们总是看到最新的数据,因为它没有本地“副本”。

非常强大的enabled选项

useQuery hook有许多选项,我们可以传入这些选项来自定义其行为,而enabled选项是一个非常强大的选项,可以让我们做许多很酷的事情(双关语)。以下是通过此选项我们能够完成的事情的简短列表:

  • 依赖查询 在一个查询中获取数据,只有在我们成功从第一个查询中成功获取数据后才运行第二个查询。
  • 打开或者关闭查询,由于refetchInterval,我们可以定期轮询数据的查询,但是如果Modal 处于打开状态,我们可以暂时暂停它以避免屏幕背面的更新。
  • 等待用户输入,在查询key中有一些过滤条件,但只要用户没有输入到他们的过滤器就中时,禁用查询。
  • 在某些用户输入后禁用查询,例如 如果我们有一个暂时不需要存储到服务器的草稿值。 请参阅上面的示例。

不要将queryCache用作本地状态管理器

如果我们篡改了queryCache(queryCache.setData),它应该只用于乐观更新或写入我们收到后端变更过的数据。 请记住,每个后台重新获取都可能覆盖该数据,因此请使用其它工具保存本地状态,例如useStateZustandRedux

创建自定义Hooks

即使它只是为了包装一个useQuery调用,创建一个自定义钩子通常也是有价值的的,因为:

  • 我们可以将数据获取和UI进行分离,可以将useQuery调用聚集在一起。
  • 我们可以将一个查询key(以及可能的类型定义)的所有引用保存在一个文件中。
  • 如果我们需要调整一些设置或添加一些数据转换,我们可以在一个地方完成。

我们已经在上面的todos查询中看到了这种例子

随着React自身函数化的推进,React生态也逐步的完善,出现了很多非常好的工具,尤其是数据获取和状态管理相关的组件。React Query将Apollo中的需要优点带入的RESTful的世界中,这让很多既有应用在不用改写为GraphQL的同时享受到了Apollo的数据缓存和服务器状态一致等优点。并且React Query并没有限定我们使用fetch或者Axios,而是在这之上提供了一个高效的封装和适配方案,不得不说这非常便于现有应用使用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK