查看原文
其他

【第2710期】React Query的实用技巧

fe小陈 前端早读课 2022-08-28

前言

React Query 系列文之一。今日前端早读课文章由字节跳动 @fe 小陈翻译分享。

正文从这开始~~

当 GraphQL 尤其是 Apollo Client 在 2018 年出现时,关于它将完全取代 redux 有很多议论,问题「Redux 死了吗?」被问了很多。

我当时不明白这是怎么回事。为什么某些数据获取库会取代我们的全局状态管理器?这两者有什么关系呢?

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

客户端状态与服务器状态

Apollo 为你提供的不仅仅是描述你想要的数据和获取该数据的能力,它还为该服务器数据提供了缓存。这意味着你可以在多个组件中使用相同的(key 相同) useQuery 钩子,它只会获取一次数据,然后从缓存中返回它。

这与我们使用 redux 的目的十分相近,许多团队,使用 redux 就是为了:从服务器获取数据并使其随处可用。

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

对我来说,这引入了如何思考数据的范式转变。如果我们可以利用缓存来显示我们不拥有的数据,那么真正需要让整个应用程序都可以使用的客户端状态就真的不多了。这让我明白为什么很多人认为 Apollo 可以在很多情况下取代 redux。

React Query

我从来没有机会使用 GraphQL 。我们有一个现有的 REST API,并没有真正遇到过度获取的问题,目前是可用的。显然,我们没有足够的痛点来切换 GraphQL,特别是考虑到你还必须适配后端修改,这不是那么简单的。

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

查找 。

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

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

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

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

默认配置

我相信 React Query Defaults 的选择非常好,但它们有时会让你措手不及,尤其是在开始的时候。

首先:即使默认的 staleTime 为零,React Query 也不会在每次重新渲染时调用 queryFn。你的应用程序可以随时出于各种原因重新渲染,因此每次获取都将是疯狂的!

始终为重新渲染编写代码,其中很多。我喜欢称之为渲染弹性。— 坦纳・林斯利

如果你看到一个你没有预料到的重新获取,很可能是因为你只是聚焦了窗口,而 React Query 正在做一个 refetchOnWindowFocus,这是一个很棒的特性:如果用户转到不同的浏览器选项卡,然后又回来了到你的应用程序,将自动触发后台重新获取,如果在此期间服务器上的某些内容发生更改,屏幕上的数据将更新。所有这一切都发生在没有显示 Loading 状态的情况下,如果数据与你当前在缓存中的数据相同,你的组件将不会重新渲染。

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

其次,cacheTime 和 staleTime 之间似乎有点混淆,所以让我试着澄清一下:

  • StaleTime:查询从新鲜转换为陈旧的持续时间。只要查询是新鲜的,数据将始终只从缓存中读取 —— 不会发生网络请求!如果查询过时(默认情况下是:立即),你仍然会从缓存中获取数据,但在某些情况下可能会发生后台重新获取。

  • CacheTime:从缓存中删除非活动查询的持续时间。这默认为 5 分钟。一旦没有观察者注册,查询就会转换到非活动状态,因此当使用该查询的所有组件都已卸载时。

大多数情况下,如果你想更改这些设置之一,则需要调整的是 staleTime。我很少需要篡改 cacheTime。文档中的示例也有很好的解释。

使用 React Query DevTools

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

将查询键视为依赖数组

我在这里指的是 useEffect 钩子的依赖数组,我假设你很熟悉。

为什么这两个相似?

因为每当查询键更改时,React Query 都会触发重新获取。因此,当我们将可变参数传递给 queryFn 时,我们几乎总是希望在该值更改时获取数据。我们可以使用查询键,而不是编写复杂的逻辑来手动触发重新获取:

// feature/todos/queries.ts
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 会自动为我们触发重新获取,因为查询键 (上面的 state) 发生了变化。因此,我们将用户的过滤器选择与查询函数保持同步,这与 useEffect 依赖数组表示的内容非常相似。我从来没有将一个不属于 queryFn 的 key 变量传递给 queryFn 。

一个新的缓存条目

因为查询键用作缓存的键,所以当你从 'all' 切换到 'done' 时,你将获得一个新的缓存条目,这将导致硬加载状态(可能显示加载微调器)。这当然不理想,因此你可以在这些情况下使用 keepPreviousData 选项,或者,如果可能,使用 initialData 预填充新创建的缓存条目。上面的例子非常适合这个,因为我们可以对我们的待办事项做一些客户端预过滤:

// pre-filtering
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 属性以实际触发后台提取。

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

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

这与 put-props-to-use-state 密切相关,这是我上个月写的一篇文章:如果你从 useQuery 获取数据,请尽量不要将该数据放入本地状态。主要原因是你隐式地选择退出 React Query 为你所做的所有后台更新,因为状态 “复制” 不会随之更新。

如果你想,这很好,例如获取表单的一些默认值,并在获得数据后呈现你的表单。后台更新不太可能产生新的东西,即使你的表单已经初始化。因此,如果你是故意这样做的,请确保不要通过设置 staleTime 来触发不必要的后台重新获取:

初始形式数据
// initial-form-data
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.io/s/separate-…

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

启用选项非常强大

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

1、相关查询

在一个查询中获取数据,只有在我们成功从第一个查询中获取数据后才运行第二个查询。

2、打开和关闭查询

由于 refetchInterval,我们有一个定期轮询数据的查询,但是如果 Modal 处于打开状态,我们可以暂时暂停它以阻止当前的更新。

3、等待用户输入

在查询键中有一些过滤条件,但只要用户没有应用他们的过滤器就禁用它。

4、在某些用户输入后禁用查询

例如如果我们有一个应优先于服务器数据的草稿值。请参阅上面的示例。

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

不要轻易篡改 queryCache (queryCache.setData),它应该只用于乐观更新或写入你在 mutation 后从后端收到的数据。请记住,每个后台重新获取都可能覆盖该数据,因此请使用其他内容作为本地状态。

创建自定义 Hook

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

  • 你可以保留从 UI 中获取的实际数据,但与你的 useQuery 调用位于同一位置。

  • 你可以将一个查询键(以及可能的类型定义)的所有用法保存在一个文件中。

  • 如果你需要调整一些设置或添加一些数据转换,你可以在一个地方完成。你已经在上面的 todos 查询中看到了一个例子。

我希望这些实用技巧能帮助你开始使用 React Query,去尝试一下吧。

关于本文
译者:@fe 小陈
译文:https://juejin.cn/post/7025078863025668110
作者:@TkDodo
原文:https://tkdodo.eu/blog/practical-react-query

前端开发是一个庞大的体系,纷繁复杂的知识点铸成了一张信息密度极高的图谱。在开发过程中,一行代码就可能使宿主引擎陷入性能瓶颈;团队中的代码量呈几何级数式增长,可能愈发尾大不掉,掣肘业务的发展。这些技术环节,或宏观或微观,都与工程化基建、架构设计息息相关。关于这些尽在【图书】前端架构师:基础建设与架构设计思想

欢迎读者自荐投稿,前端早读课等你来

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存