实用的 React Query 客户端状态与服务器状态 React Query

2025-06-07

实用 React 查询

客户端状态与服务器状态

反应查询

当 GraphQL 和Apollo Client在 2018 年左右开始流行时,人们对它完全取代 redux 大惊小怪,并且“Redux 死了吗? ”这个问题被问了很多次。

我清楚地记得我当时不明白这到底是怎么回事。为什么某个数据获取库会取代你的全局状态管理器?这两者之间又有什么关系呢?

我的印象是,像 Apollo 这样的 GraphQL 客户端只会为您获取数据,类似于axios对 REST 所做的操作,并且您显然仍然需要某种方式让您的应用程序可以访问这些数据。

我真是大错特错。

客户端状态与服务器状态

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

这听起来非常熟悉,我们以及可能许多其他团队主要使用redux来做以下事情:从服务器获取数据并使其在任何地方可用。

所以看起来我们一直以来都把这个服务器状态当作其他客户端状态来对待。只不过,当涉及到服务器状态时(比如:你获取的文章列表、你想要显示的用户的详细信息等等),你的应用并不拥有它。我们只是借用它,在屏幕上为用户显示它的最新版本。服务器才是数据的拥有者。

对我来说,这带来了数据思维模式的转变。如果我们可以利用缓存来显示不属于我们的数据,那么就不需要将真正的客户端状态提供给整个应用了。这让我理解了为什么很多人认为 Apollo 在很多情况下可以取代 Redux。

反应查询

我从来没有机会使用 GraphQL。我们有一个现有的 REST API,很少遇到过度获取的问题,它一直都能正常工作等等。显然,我们并没有太多痛点需要切换,尤其是考虑到你还需要调整后端,而这可不是那么简单。

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

输入React Query

React Query由开源开发者Tanner Linsley于 2019 年末开发,它汲取了 Apollo 的精髓,并将其融入 REST 框架。它兼容任何返回 Promise 的函数,并采用stale-while-revalidate缓存策略。该库采用合理的默认设置,力求尽可能保持数据新鲜,同时尽早向用户显示数据,使其有时感觉近乎即时,从而提供出色的用户体验。此外,它还非常灵活,当默认设置不足时,您可以自定义各种设置。

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

我认为文档非常善于解释指南和概念,你可以观看各种演讲的视频
如果你想熟悉该库,Tanner 有一个 React Query Essentials 课程可以参加。

我想更多地关注一些超出文档范围的实用技巧,即使你已经在使用该库,这些技巧也可能对你有所帮助。这些都是我在过去几个月里积累的经验,当时我不仅在工作中积极使用该库,还积极参与 React Query 社区,在 Discord 和 GitHub 讨论区回答问题。

默认设置解释

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

首先:React Query 不会每次重新渲染时都调用 queryFn,即使默认的staleTime为零。你的应用可能随时因为各种原因重新渲染,所以每次都去获取数据会很疯狂!

始终为重新渲染编写代码,而且要多次重新渲染。我喜欢称之为“渲染弹性”。

— 坦纳·林斯利

如果你看到了意料之外的重新加载,很可能是因为你刚刚将窗口聚焦,而 React Query 正在执行refetchOnWindowFocus。这对于生产环境来说是一个很棒的功能:如果用户切换到其他浏览器标签页,然后返回到你的应用,就会自动触发后台重新加载。如果在此期间服务器上的数据发生了变化,屏幕上的数据也会随之更新。所有这些都不会显示加载旋转图标,而且如果数据与当前缓存中的数据相同,你的组件就不会重新渲染。

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

其次, cacheTimestaleTime之间似乎有点混淆,所以让我尝试澄清一下:

  • StaleTime:查询从“新鲜”状态变为“过时”状态的持续时间。只要查询是新鲜的,数据就始终只从缓存中读取 - 不会发生网络请求!如果查询已过时(默认情况下为:立即),您仍然可以从缓存中获取数据,但在某些情况下可能会发生后台重新获取。
  • CacheTime:非活动查询从缓存中移除前的等待时间。默认值为 5 分钟。一旦没有注册观察者,查询就会立即转换为非活动状态,也就是说,当所有使用该查询的组件都卸载后。

大多数情况下,如果你想更改其中一个设置,只需要调整staleTime即可。我很少需要修改cacheTime 。文档中也有一个很好的示例解释。

使用 React Query DevTools

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

将查询键视为依赖项数组


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

这两者为何相似?

因为每当查询键发生变化时,React Query 都会触发重新获取。所以当我们将变量参数传递给 queryFn 时,我们几乎总是希望在该值发生变化时获取数据。与其编排复杂的效果来手动触发重新获取,不如利用查询键:

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))
Enter fullscreen mode Exit fullscreen mode

假设我们的 UI 显示一个待办事项列表以及一个过滤选项。我们会使用一些本地状态来存储这些过滤选项,一旦用户更改选择,我们就会更新该本地状态,并且 React Query 会自动触发重新获取,因为查询键会发生变化。这样,我们就能将用户的过滤选项与查询函数保持同步,这与 useEffect 的依赖数组非常相似。我记得我从未将属于 queryKey 的变量传递给 queryFn。

新的缓存条目

由于 quey 键被用作缓存的键,因此当您从“全部”切换到“完成”时,您将获得一个新的缓存条目,这会导致您首次切换时出现硬加载状态(可能显示加载旋转图标)。这当然不是理想的情况,因此您可以使用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 = queryCache.getQuery<Todos>(['todos', 'all'])
            const filteredData = allTodos?.filter((todo) => todo.state === state) ?? []

            return filteredData.length > 0 ? filteredData : undefined
        },
    })
Enter fullscreen mode Exit fullscreen mode

现在,每次用户在状态之间切换时,如果我们还没有数据,我们会尝试使用“所有待办事项”缓存中的数据进行预填充。这样,我们就可以立即向用户显示已完成的待办事项,并且即使后台获取完成,用户仍然会看到更新后的列表。请注意,在 v3 之前的版本中,您还需要设置initialStale属性才能真正触发后台获取。

我认为仅用几行代码就能实现很好的用户体验改进。

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

这与我上个月写的一篇文章“将 props 放入 use-state”相辅相成:如果你从useQuery获取数据,尽量不要将这些数据放入本地 state。主要原因是,这会隐式地退出 React Query 为你执行的所有后台更新,因为 state 的“副本”不会随之更新。

例如,如果你想为表单获取一些默认值,并在获得数据后渲染表单,这样做是可以的。后台更新不太可能产生新内容,即使产生新内容,你的表单也已经初始化了。所以,如果你故意这样做,请确保通过设置staleTime来避免触发不必要的后台重新加载

const App = () => {
    const { data } = useQuery('key', queryFn, { staleTime: Infinity })

    return data ? <MyForm initialData={data} /> : null
}

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

当你显示数据并且希望允许用户编辑时,这个概念会有点难以理解,
但它有很多优点。我准备了一个 codeandbox 的小例子:

本演示的重点在于,我们永远不会将从 React Query 获取的值放入本地状态。这确保我们始终看到最新数据,因为本地不存在任何“副本”。

启用的选项非常强大

useQuery钩子有很多选项可以传入,用于自定义其行为。其中enabled选项非常强大,可以你实现很多很酷的功能(此处双关)。以下是我们通过这个选项实现的一些功能:

  • 依赖查询在一个查询中获取数据,并且只有在我们成功从第一个查询中获取数据后才运行第二个查询。
  • 打开和关闭查询我们有一个查询,通过refetchInterval定期轮询数据,但如果打开了 Modal,我们可以暂时暂停它,以避免在屏幕后面进行更新。
  • 等待用户输入在查询键中有一些过滤条件,但只要用户没有应用他们的过滤器就禁用它。
  • 在用户输入某些内容后禁用查询,例如,如果我们有一个草稿值,该值应优先于服务器数据。参见上面的例子。

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

如果您篡改 queryCache(queryCache.setData),则应仅将其用于乐观更新,或用于写入在变更后从后端收到的数据。请记住,每次后台重新获取都可能会覆盖该数据,因此请使用 其他方法 来保存本地状态。

创建自定义钩子

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

  • 您可以保留从 UI 中提取的实际数据,但与您的useQuery调用位于同一位置。
  • 您可以将一个查询键(以及潜在的类型定义)的所有用法保存在一个文件中。
  • 如果您需要调整一些设置或添加一些数据转换,您可以在一个地方进行。

您已经在上面的 todos 查询中看到了一个示例


我希望这些实用技巧能够帮助您开始使用 React Query,所以去查看一下吧 :) 如果您还有其他问题,请在下面的评论中告诉我⬇️

文章来源:https://dev.to/tkdodo/practical-react-query-8lg
PREV
简化 useEffect 1. 编写更少的效果 2. 遵循单一责任原则 3. 编写自定义钩子 4. 为它们命名 5. 不要对依赖关系撒谎
NEXT
宣布推出 TinaCMS