React-Query-状态管理-一-

57 阅读55分钟

React Query 状态管理(一)

原文:zh.annas-archive.org/md5/7a61313ab658bb102a1a310c5d41c0f9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

状态管理是 React 生态系统中最热门的话题之一。在 React 中处理状态有许多库和工具,每个都带来了不同的配方和观点。有一点是明确的——处理客户端状态的状态管理解决方案并不擅长处理服务器状态。React Query 就是为了解决这个问题而创建的,以帮助管理你的服务器状态!

使用 React Query 进行状态管理中,你将获得一本指南,它将带你从零开始,到本书结束时成为 React Query 的专家。

你将从学习 React 世界中状态的历史背景以及是什么导致了从全局状态到客户端和服务器状态的转变开始。有了这些知识,你将理解 React Query 的需求。随着你通过章节的深入,你将学习 React Query 如何帮助你处理常见的服务器状态挑战,例如获取数据、缓存数据、更新数据以及将数据与服务器同步。

但这还不是全部——一旦你掌握了 React Query,你将学习如何将这一知识应用到服务器端渲染中。

最后,你将通过利用 Testing Library 和 Mock Service Worker 来学习一些测试代码的模式。

在本书结束时,你将获得对状态的新视角,并能够利用 React Query 解决应用程序中服务器状态的所有挑战。

本书面向对象

本书面向希望提高状态管理技能并开始处理服务器状态挑战,同时提升开发和 用户体验的 JavaScript 和 React 开发者。*

对 Web 开发、JavaScript 和 React 的基本了解将有助于理解本书中涵盖的一些关键概念。

本书涵盖内容

第一章什么是状态以及我们如何管理它?,涵盖了状态的基本定义,并给出了我们如何管理状态的历史概述。

第二章服务器状态与客户端状态对比,将状态概念分开,帮助我们理解为什么独立于客户端状态管理服务器状态如此重要。

第三章React Query – 介绍、安装和配置,介绍了 React Query 并提供将其添加到应用程序的方法。

第四章使用 React Query 获取数据,涵盖了如何利用useQuery自定义钩子获取你的服务器状态。

第五章更多数据获取挑战,扩展了前一章中介绍的概念,并涵盖了如何利用useQuery处理其他数据获取挑战。

第六章使用 React Query 执行数据突变,涵盖了如何利用useMutation自定义钩子对服务器状态进行更改。

第七章使用 Next.js 或 Remix 进行服务器端渲染,介绍了如何利用 React Query 与 Next.js 或 Remix 等服务器端框架。

第八章测试 React Query 钩子和组件,为你提供了可以将应用到你的应用程序中以测试你的组件和利用 React Query 的自定义钩子的实践和食谱。

第九章React Query v5 的变化是什么?,是一个附加章节,介绍了 TanStack Query 的 v5 版本对 React Query 的引入以及你需要更新应用程序的内容。

为了充分利用这本书

建议具备基本的 RESTful API 和 HTTP 方法知识。如果你想利用使用它的示例,则需要具备基本的 GraphQL 知识。

你需要了解一些关于 HTML 的基本概念。你还需要理解 JavaScript 以及其一些概念,特别是承诺。

最后,鉴于我们正在使用 React Hooks,了解它们的工作原理以及如何在你的 React 应用程序中使用它们非常重要。

本书涵盖的软件/硬件操作系统要求
YarnWindows, macOS, 或 Linux
pnpmWindows, macOS, 或 Linux
npmWindows, macOS, 或 Linux
JavaScriptWindows, macOS, 或 Linux
React 16.8Windows, macOS, 或 Linux
RemixWindows, macOS, 或 Linux
Next.jsWindows, macOS, 或 Linux
React Testing LibraryWindows, macOS, 或 Linux
Mock Service WorkerWindows, macOS, 或 Linux
TanStack QueryWindows, macOS, 或 Linux

如果你使用的是本书的数字版,我们建议你亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)访问代码。这样做将帮助你避免与代码的复制和粘贴相关的任何潜在错误。

本书为你提供了实践和工具,以全面理解和掌握 TanStack Query React 适配器——React Query。到本书结束时,你将具备充分利用它的必要理解,并准备好决定是否将其添加到你的项目中。

下载示例代码文件

你可以从 GitHub 下载这本书的示例代码文件:github.com/PacktPublis… GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包,可在 github.com/PacktPublis… 上找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表的彩色图像 PDF 文件。你可以从这里下载:packt.link/Wt1n6

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“React 原生地为我们提供了两种在应用程序中保持状态的方法 - useStateuseReducer。”

代码块设置如下:

const NotState = ({aList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}) => {
  const value = "a constant value";
  const filteredList = aList.filter((item) => item % 2 === 0);
  return filteredList.map((item) => <div key={item}>{item}</div>);
};

当我们希望将您的注意力引到代码块中的特定部分时,相关的行或项目将以粗体显示:

const App = () => {
  ...
  return (
    <div className="App">
      <div>Counter: {count}</div>
      <div>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>

任何命令行输入或输出都按以下方式编写:

npm i @tanstack/react-query

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“作为一个用户,我希望在点击无效****查询按钮时重新获取我的查询。”

小贴士或重要注意事项

它看起来像这样。

联系我们

我们始终欢迎读者的反馈。

请将邮件主题中提及书籍标题,并发送至customercare@packtpub.com

勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/support/err… 并填写表格。

请通过copyright@packt.com发送带有材料链接的邮件。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。

分享您的想法

一旦您阅读了《使用 React Query 进行状态管理》,我们非常乐意听到您的想法!扫描下面的二维码直接进入此书的亚马逊评论页面并分享您的反馈。

二维码图片

packt.link/r/1-803-23134-3

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法随身携带您的印刷书籍吗?

您的电子书购买是否与您选择的设备不兼容?

请放心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限

按照以下简单步骤获取福利:

  1. 扫描下面的二维码或访问以下链接

二维码图片

packt.link/free-ebook/9781803231341

  1. 提交您的购买证明

  2. 就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱

第一部分:理解状态和了解 React Query

状态是使您的应用程序运行的关键。我们很多人往往没有意识到的是,存在不同类型的状态。这些不同类型的状态在管理给定状态时会导致不同的挑战。在本部分,我们将更深入地了解状态以及我们如何管理它。在这个过程中,我们将了解到服务器和客户端状态具有非常不同的挑战,需要分别处理,并使用不同的工具。为了处理服务器状态,我们将学习更多关于 TanStack Query React 适配器 React Query 的知识,并了解如何将其添加到我们的应用程序中。

本部分包括以下章节:

  • 第一章什么是状态以及我们如何管理它?

  • 第二章服务器状态与客户端状态

  • 第三章React Query – 介绍、安装和配置

第一章:状态是什么以及我们如何管理它?

状态是一个可变的数据源,可以用于在 React 应用程序中存储数据,并且可以随时间变化,并可用于确定你的组件如何渲染。

本章将更新你对 React 生态系统中的状态的现有知识。我们将回顾它是什么以及为什么需要它,并了解它是如何帮助你构建 React 应用的。

我们还将回顾如何通过使用 useState 钩子、useReducer 钩子和 React Context 原生地管理状态。

最后,我们将简要介绍常见的状态管理解决方案,如 ReduxZustandMobX,并了解它们为什么被创建以及它们共有的主要概念。

到本章结束时,你将学习或记住关于状态的所有必要知识,以便继续阅读本书。你还会注意到不同状态管理解决方案之间状态管理的方式存在一种模式,或者重新认识一个熟悉的术语。剧透一下:它是全局状态。

在本章中,我们将涵盖以下主题:

  • React 中的状态是什么?

  • 在 React 中管理状态

  • 不同的状态管理库有什么共同之处?

技术要求

在本书中,你将看到一些代码片段。如果你想尝试它们,你需要以下工具:

  • 一种集成开发环境IDE)如 Visual Studio Code。

  • 一个网络浏览器(Google Chrome、Firefox 或 Edge)。

  • Node.js。本书中的所有代码都是使用当前 LTS 版本编写的(16.16.0)。

  • 一个包管理器(npm、Yarn 或 pnpm)。

  • 一个 React 项目。如果你没有,你可以在终端中运行以下命令来创建一个:

    npx create-react-app my-react-app
    

本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/State-management-with-React-Query/tree/feat/chapter_1

React 中的状态是什么?

状态是你的 React 应用的核心。

我挑战你尝试构建一个没有任何类型状态的 React 应用程序。你可能能够做些什么,但很快你就会得出结论,props 不能为你做所有事情,然后陷入困境。

如介绍中所述,状态是一个可变的数据源,用于存储你的数据。

状态是可变的,这意味着它可以随时间变化。当状态变量发生变化时,你的 React 组件将重新渲染以反映状态对 UI 造成的任何更改。

好的,现在,你可能想知道,“我在状态中会存储什么?”嗯,我遵循的一个经验法则是,如果你的数据符合以下任何一点,那么它就不是状态:

  • Props

  • 总是相同的 数据

  • 可以从其他状态变量或 props 推导出的数据

任何不符合此列表的内容都可以存储在状态中。这意味着像通过请求获取的数据、UI 的浅色或深色模式选项以及从 UI 表单中填写错误得到的错误列表等都是状态的例子。

让我们看看以下示例:

const NotState = ({aList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10
  ]}) => {
  const value = "a constant value";
  const filteredList = aList.filter((item) => item % 2 ===
    0);
  return filteredList.map((item) =>
    <div key={item}>{item}</div>);
};

这里,我们有一个名为 NotState 的组件。让我们看看里面的值,并使用我们的经验法则。

aList 变量是一个组件属性。由于我们的组件将接收这个属性,因此它不需要是状态。

我们的 value 变量被分配了一个字符串值。由于这个值始终是 常量,因此它不需要是状态。

最后,filteredList 变量是从我们的 aList 属性中派生出来的;因此,它不需要是状态。

现在你已经熟悉了状态的概念,让我们动手了解如何在 React 中管理它。

在 React 中管理状态

在深入一些示例之前,重要的是要提到,在这本书中,所有展示的示例都是 React 16.8 版本之后的版本。这是因为 React Hooks 是在这个版本中引入的。Hooks 改变了我们编写 React 代码的方式,并允许出现像 React Query 这样的库,因此任何展示的示例都利用了它们。

什么是 React Query?

React Query 是一个用于在 React 中获取、缓存和更新服务器状态的协议无关的钩子集合。

在本节中,我将向您展示 React 如何在组件中处理状态,以及如果我们需要在组件之间共享状态时应该做什么。

让我们考虑以下场景。

我想构建一个允许我计数的应用程序。在这个应用程序中,我想能够做以下事情:

  • 查看当前计数器值

  • 增加我的计数器

  • 减少我的计数器

  • 重置计数器

让我们想象我们有一个名为 App 的 React 组件:

const App = () => {
  ...
  return (
    <div className="App">
      <div>Counter: {count}</div>
      <div>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>

这个应用程序提供了处理我们的计数器需求所需的 UI,例如一个 div,我们应该用它来显示我们的 count,以及三个带有 onClick 事件的按钮,等待回调函数执行以下所需的每个动作。

我们只是缺少这个组件的核心,即状态。React 本地为我们提供了两种在应用程序中保存状态的方法:useStateuseReducer

让我们从查看 useState 开始。

使用 useState 管理状态

useState 是一个 React 钩子,允许你保存一个有状态值。当你调用这个钩子时,它将返回一个有状态值和一个用于更新它的函数。

让我们看看如何利用 useState 构建计数器应用的示例:

const App = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount((currentCount) =>
    currentCount + 1);
  const decrement = () => setCount((currentCount) =>
    currentCount - 1);
  const reset = () => setCount(0);
  return (
    <div className="App">
      <div>Counter: {count}</div>
      <div>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
};

前面的代码片段利用 useState 钩子来保存我们的计数器状态。当我们第一次调用 useState 时,会做两件事:

  • 状态值初始化为 0

  • count 状态变量被解构;然后,对状态更新函数,称为 setCount,也做了同样的处理

在此之后,我们声明函数,在其中我们使用状态更新函数setCount来增加、减少或重置我们的状态变量。

最后,我们将状态变量分配给相应的 UI 部分,并将回调传递给按钮的onClick事件。

通过这种方式,我们已经构建了一个简单的计数器应用。我们的应用将开始渲染计数为 0。每次我们点击按钮时,它将执行相应的状态更新,重新渲染我们的应用,并显示新的计数值。

useState是你在 React 应用中需要任何状态时最常用的答案。但别忘了在应用之前先应用“我在状态中要存储什么?”的经验法则!

现在,让我们看看如何使用useReducer钩子来管理状态并构建相同的计数器应用的例子。

使用 useReducer 管理状态

当我们有一个更复杂的状态时,useReducer是首选选项。在使用钩子之前,我们需要做一些设置,以便我们有发送到useReducer钩子所需的一切:

const initialState = { count: 0 };
const types = {
  INCREMENT: "increment",
  DECREMENT: "decrement",
  RESET: "reset",
};
const reducer = (state, action) => {
  switch (action) {
    case types.INCREMENT:
      return { count: state.count + 1 };
    case types.DECREMENT:
      return { count: state.count - 1 };
    case types.RESET:
      return { count: 0 };
    default:
      throw new Error("This type does not exist");
  }
};

在上述代码片段中,我们创建了三件事:

  • 一个initialState对象。该对象有一个属性 count,其值为0

  • 一个描述我们将支持的所有操作类型的types对象。

  • 一个reducer。这个 reducer 负责接收我们的状态和操作。通过匹配该操作与预期的类型,我们将能够更新状态。

现在设置完成,让我们创建我们的计数器:

const AppWithReducer = () => {
  const [state, dispatch] = useReducer(reducer,
    initialState);
  const increment = () => dispatch(types.INCREMENT);
  const decrement = () => dispatch(types.DECREMENT);
  const reset = () => dispatch(types.RESET);
  return (
    <div className="App">
      <div>Counter: {state.count}</div>
      <div>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
};

上述代码片段利用useReducer钩子来保存我们的计数器状态。当我们第一次调用useReducer时,会做三件事:

  • 我们向我们的钩子指示应该使用哪个reducer

  • 我们使用initialState对象初始化我们的状态。

  • 我们解构state对象和dispatch函数,这使得我们可以从useReducer钩子中分发操作。

在此之后,我们创建负责调用带有预期操作的dispatch函数的函数。

最后,我们将状态变量分配给相应的 UI 部分,并将回调传递给按钮的onClick事件。

现在你已经掌握了这两个钩子,你现在知道如何在组件中管理状态。

现在,让我们设想以下场景:如果你需要你的计数器状态在其他组件中可访问怎么办?

你可以通过 props 传递它们。但如果这个状态需要发送到树上的五个其他组件和不同级别呢?你会进行 prop-drilling 并将它传递给每个组件吗?

为了处理这种场景并提高代码的可读性,React Context被创建出来。

使用 React Context 共享状态

Context 允许你在不进行 prop-drilling 的情况下在组件之间原生地共享值。让我们学习如何构建一个上下文来处理我们的计数器:

import { useState, createContext } from "react";
export const CountContext = createContext();
export const CountStore = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount((currentCount) =>
    currentCount + 1);
  const decrement = () => setCount((currentCount) =>
    currentCount - 1);
  const reset = () => setCount(0);
  return {
    count,
    increment,
    decrement,
    reset,
  };
};
const CountProvider = (children) => {
  return <CountContext.Provider value={CountStore()}
    {...children} />;
};
export default CountProvider;

在上述代码片段中,我们做了三件事:

  • 使用createContext函数创建我们的上下文。

  • 创建一个useState钩子。在存储的末尾,我们返回一个包含执行状态更新和创建我们的状态变量的函数的对象。

  • 创建一个CountProvider。这个提供者负责创建一个将用于包裹组件的提供者。这将允许该提供者内部的每个组件都能访问我们的CountStore值。

一旦完成这个设置,我们需要确保我们的组件可以访问我们的上下文:

root.render(
  <CountProvider>
    <App />
  </CountProvider>
);

前面的代码片段利用了我们在前面的代码片段中创建的CountProvider来包裹我们的App组件。这允许App内部的每个组件都能消费我们的上下文:

import { CountContext } from "./CountContext/CountContext";
const AppWithContext = () => {
  const { count, increment, decrement, reset } =
    useContext(CountContext);
  return (
    <div className="App">
      <div>Counter: {count}</div>
      <div>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
};

最后,在这个代码片段中,我们利用了useContext钩子来消费我们的CountContext。由于我们的组件是在我们的自定义提供者中渲染的,因此我们可以访问上下文中的状态。

每当上下文中的状态更新时,React 将确保所有消费我们的上下文的组件都会重新渲染,并接收状态更新。这往往会导致不必要的重新渲染,因为如果你只消费状态中的一个变量,而由于某种原因另一个变量发生了变化,那么上下文将迫使所有消费者重新渲染。

上下文的一个缺点是,通常,无关的逻辑会聚集在一起。正如你可以从前面的代码片段中看到的那样,这会以牺牲一些样板代码为代价。

现在,上下文仍然很强大,这是 React 如何让你在组件之间共享状态的方式。然而,它并非一直存在,因此社区不得不想出如何实现状态共享的方法。为此,创建了状态管理库。

不同的状态管理库有什么共同之处?

React 为你提供的一个自由是,它不对你的开发强加任何标准或实践。虽然这很好,但它也导致了不同的实践和实现。

为了使这更容易,并为开发者提供一些结构,创建了状态管理库:

  • Redux提倡一个以存储、reducer 和选择器为重点的方法。这导致需要学习特定的概念,并在项目中填充大量可能影响代码可读性和增加代码复杂性的样板代码。

  • Zustand提倡使用自定义钩子方法,其中每个钩子都持有你的状态。这是迄今为止最简单的解决方案,也是我最喜欢的解决方案。它与 React 协同工作,并完全拥抱钩子。

  • MobX不强制架构,而是关注于函数式响应式方法。这导致了一些更具体的概念,实践方式的多样性可能导致开发者遇到与 React 中可能已经遇到的相同的代码结构问题。

所有这些库中有一个共同点,那就是它们都在尝试解决我们尝试用 React Context 解决的问题:管理我们的 共享状态 的方法

在 React 树内部可访问多个组件的状态通常被称为全局状态。现在,全局状态常常被误解,这导致你的代码中增加了不必要的复杂性,并且常常需要求助于本节中提到的库。

最后,每个开发者和团队都有自己的偏好和选择。考虑到 React 给你提供了处理状态的自由,你必须考虑每个解决方案的所有优缺点,在做出选择之前。从一个迁移到另一个可能需要大量时间,并完全改变你应用程序中处理状态的模式,所以请明智地选择,并留出足够的时间。

虽然全局状态不是 React Query 被构建的原因,但它对其创建有影响。全局状态通常的组成方式导致了管理其特定部分的需要,这部分有许多挑战。这个特定部分被称为服务器状态,而它历史上的处理方式为激励 Tanner Linsley 创建 React Query 铺平了道路。

摘要

在本章中,我们熟悉了状态的概念。到目前为止,你应该理解状态作为 React 应用程序核心的重要性,并知道如何借助useStateuseReducer原生化地管理它。

你了解到有时你需要与多个组件共享状态,你可以通过 Context 或利用第三方状态管理库来实现。每种解决方案都有其优缺点,最终将取决于开发者的个人偏好。

第二章《服务器状态与客户端状态》中,你将更深入地了解全局状态,并发现我们的全局状态通常是服务器和客户端状态的组合。你将学习这些术语的含义,如何识别这些状态,以及与它们相关的常见挑战。

第二章:服务器状态与客户端状态的比较

全局状态是我们看待状态最常见的方式。它是通过一个或多个组件在我们的应用程序中全局共享的状态。

我们通常不知道的是,在我们的日常开发中,我们的全局状态最终会在我们的应用程序外部持久化的状态和仅存在于我们应用程序内的状态之间分割。第一种类型的状态被称为服务器状态,而第二种类型的状态被称为客户端状态。这两种类型的状态都有它们特定的挑战,并需要不同的工具来帮助管理它们。

在本章中,我们将了解为什么我们主要将状态称为全局状态,以及为什么我们应该调整我们的思维模型以包括客户端和服务器状态。

我们还将回顾每种类型的状态负责什么,如何在应用程序中区分它们,以及理解导致 React Query 创建的挑战。

到本章结束时,你将能够通过应用你刚刚学到的思维模型,将全局状态完全分割成客户端状态和服务器状态。

你还将了解在应用程序中拥有服务器状态所创造的所有挑战,并准备好用 React Query 克服它们。

在本章中,我们将涵盖以下主题:

  • 什么是全局状态?

  • 什么是客户端状态?

  • 什么是服务器状态?

  • 理解与服务器状态相关的常见挑战

技术要求

本章的所有代码示例都可以在 GitHub 上找到,链接为github.com/PacktPublishing/State-management-with-React-Query/tree/feat/chapter_2

什么是全局状态?

当我们在 React 世界中开始状态管理时,我们通常不熟悉不同的概念。

通常,我们只是通过思考我们在组件中拥有的useStateuseReducer钩子的数量来查看状态。然后,当useStateuseReducer模式停止工作,我们需要在更多组件之间共享状态时,我们要么将状态提升到最近的父组件,当这个状态只需要该组件的子组件时,要么找到一个共同的地方,这个状态可以存在,并且所有我们想要的组件都可以访问它。这种状态通常被称为全局状态。

让我们看看一个应用程序中全局状态可能是什么样子的例子。在这里,我们有一个负责管理主题选择、获取数据和跟踪此获取请求加载状态的商店:

const theme = {
  DARK: "dark",
  LIGHT: "light",
};
export const GlobalStore = () => {
  const [selectedTheme, setSelectedTheme] = useState
    (theme.LIGHT);
  const [serverData, setServerData] = useState(null);
  const [isLoadingData, setIsLoadingData] = useState
    (false);
  const toggleTheme = () => {
    setSelectedTheme((currentTheme) =>
      currentTheme === theme.LIGHT ? theme.DARK :
        theme.LIGHT
    );
  };
  const fetchData = (name = "Daniel") => {
    setIsLoadingData(true);
    fetch(`<insert_url_here>/${name}`)
      .then((response) => response.json())
      .then((responseData) => {
        setServerData(responseData);
      })
 .finally(() => {
        setIsLoadingData(false);
      })
      .catch(() => setIsLoadingData(false));
  };
  useEffect(() => {
    fetchData();
  }, []);
  return {
    selectedTheme,
    toggleTheme,
    serverData,
    isLoadingData,
    fetchData
  };
};

这个片段展示了某些典型全局状态的一个示例。通过使用 React Context,我们创建了一个包含以下内容的商店:

  • 一个名为selectedTheme的状态变量,用于管理所选主题

  • 一个名为serverData的状态变量,用于显示从我们的 API 请求返回的数据

  • 一个名为isLoadingData的状态变量,用于显示我们的 API 请求当前加载状态是否仍在加载。

  • 一个名为toggleTheme的函数,允许我们在浅色和深色模式之间切换。

  • 一个fetchData函数,允许我们获取给定数据并设置我们的加载状态为truefalse,这取决于请求的状态。

  • 一个useEffect钩子,它将触发初始数据获取以提供我们的serverData状态。

useEffect 是什么?

useEffect是一个 React 钩子,允许你在组件中执行副作用。

所有这些都是从我们的存储中返回的,以便上下文的消费者可以在整个应用程序中访问它们,只要他们订阅我们的上下文。

从第一眼看来,这个状态似乎没有问题,并且可能对大多数应用程序来说已经足够了。问题是,大多数时候,这个状态会因为新的开发需求而增长。这通常会导致我们的状态大小增加。

现在,让我们设想我们需要一个次要主题,并且需要添加另一个名为secondaryTheme的状态变量。我们的代码看起来会像这样:

const [selectedTheme, setSelectedTheme] = useState(theme.LIGHT);
const [secondaryTheme, setSecondaryTheme] = useState(theme.LIGHT);
…
  const toggleSecondaryTheme = () => {
    setSecondaryTheme((currentTheme) =>
      currentTheme === theme.LIGHT ? theme.DARK :
        theme.LIGHT
    );
  };
  const toggleTheme = () => {
    setSelectedTheme((currentTheme) =>
      currentTheme === theme.LIGHT ? theme.DARK :
        theme.LIGHT
    );
  };

因此,在这个片段中,我们添加了我们的secondaryTheme状态变量,它的工作方式非常类似于selectedTheme

现在,我们在这里使用上下文;这意味着每次我们触发状态更新时,任何消费这个状态的组件都将被迫重新渲染以接收新的状态更新。这对我们意味着什么?

让我们设想我们有两个组件(让我们称它们为组件 A组件 B)正在消费这个上下文,但组件 B只解构selectedTheme状态,而组件 A解构一切。如果组件 AsecondaryTheme上触发状态更新,那么组件 B也将重新渲染,因为 React 注意到了它们共享的上下文中的更新。

这就是 React Context 的工作方式,我们无法改变这一点。我们可以争论,我们可以要么分割上下文,要么将订阅的组件分割成两个组件,并将第二个组件包裹在memo中,或者只是将我们的返回包裹在useMemo钩子中。当然,这可能会解决我们的问题,但我们只是在处理一种创建全局状态的状态类型的变化。

memo 和 useMemo 是什么?

memo是一个你可以将其包裹在组件中来定义其记忆化版本的函数。这将保证你的组件只有在它的属性发生变化时才会重新渲染。

useMemo是一个 React 钩子,允许你记忆化一个值。通常,我们想要记忆化的值是昂贵计算的结果。

现在,假设我们需要添加另一个 API 请求上下文。同样,上下文增长,我们最终会遇到与主题相同的问题。

如您现在可能已经理解的那样,状态组织有时可能是一个噩梦。我们可以求助于第三方库来帮助我们,但,再次强调,这仅仅是我们状态问题的一小部分。

到目前为止,我们只处理状态的组织,但现在想象一下,我们需要缓存我们从 API 请求中获得的数据。这可能会让我们陷入疯狂。

从我们刚刚注意到的问题中,我们可以看到,在我们的全局状态中,我们往往面临不同的挑战,一个解决方案可能对某件事有效,但对另一件事可能无效。这就是为什么分割我们的全局状态很重要。我们的全局状态通常是客户端状态和服务器状态的混合。在接下来的章节中,你将了解这些状态中的每一个是什么,我们将专注于服务器状态,最终理解为什么 React Query 如此受欢迎,并使我们的开发者生活变得更加容易。

客户端状态是什么?

我知道,到现在为止,你一定在想,这本书什么时候会开始介绍 React Query?我们几乎到了,我向你保证。我只是需要你完全理解为什么我如此热爱 React Query,而要做到这一点,了解它解决的主要问题非常重要。

现在,客户端状态不是它解决的问题之一,但你必须能够在作为开发者的日常工作中识别客户端状态,以便你完全理解应该由 React Query 管理什么,应该由其他状态管理工具管理什么。

客户端状态是应用程序拥有的状态。

这里有一些有助于定义你的客户端状态的东西:

  • 这种状态是同步的,这意味着你可以无需等待时间,通过使用同步 API 来访问它。

  • 它是局部的;因此,它只存在于你的应用程序中。

  • 它是临时的,所以页面刷新时可能会丢失,并且在会话之间通常是非持久的。

带着这些知识,如果你回顾一下GlobalStore,你会把什么识别为属于客户端状态?

可能只有selectedTheme,对吧?

让我们应用从上一个要点中学到的知识:

  • 我们需要等待获取它的值吗?,这意味着它是同步的。

  • selectedTheme只存在于我们的应用程序中吗?是的

  • 它会在页面刷新时丢失吗?是的,如果我们不在本地存储中持久化它或检查浏览器首选项,那么它的值将在页面刷新之间丢失。

考虑到这一点,我们可以肯定地说selectedTheme属于我们的客户端状态。

为了管理这种类型的状态,我们可以使用从 React Context 到 Redux、Zustand 或 MobX 等第三方库的任何东西,当事情开始变得难以组织和维护时。

如果我们对serverData状态变量提出相同的问题,它会产生相同的效果吗?

  • 数据只存在于我们的应用程序中吗?,它存在于某个地方的数据库中。

  • 它会在页面刷新时丢失吗?,数据库仍然保留着数据,所以当我们重新加载时,它将再次被检索。

  • 我们需要等待获取它吗?是的,我们需要触发一个获取此数据的请求。

这意味着我们的serverData状态变量不属于我们的客户端状态。这是我们将其归类为服务器状态的一部分。

现在,让我们谈谈让你来到这本书并使 React Query 变得必要的那个东西。

服务器状态是什么?

我们在应用程序中始终有服务器状态。主要问题是,我们试图将其与我们的客户端状态管理解决方案结合起来。试图将我们的服务器状态与我们的客户端状态管理解决方案结合的一个常见例子是使用Redux SagaRedux Thunk。它们都使得进行数据获取和存储服务器状态变得更容易。主要问题开始于我们必须处理服务器状态带来的挑战,但让我们不要走得太远;你将在下一节中了解这些挑战。

现在,你可能想知道,服务器状态是什么?

好吧,正如其名所示,服务器状态是存储在您的服务器上的状态类型。以下是一些有助于识别您的服务器状态的事情:

  • 这个状态是异步的,这意味着你需要使用异步 API 来获取和更新它。

  • 它被远程持久化——大多数情况下是在数据库或您不拥有或控制的外部位置。

  • 在你的应用程序中,这个状态不一定是最新的,因为大多数情况下,你拥有它的共享所有权,它可能被其他人改变,他们也在消费它。

带着这些知识,让我们回顾一下GlobalStore和我们的serverData状态变量,并应用这些规则来识别我们的服务器状态:

  • 我们需要异步 API 来访问这个状态吗?我们需要!我们需要向服务器发送一个获取请求并等待它发送数据回来。

  • 它是否被远程持久化?当然是的。就像我在上一个要点中说的那样,我们需要向我们的服务器请求它。

  • 这个状态在我们应用程序中总是最新的吗?我们不知道。我们无法控制状态。这意味着如果任何消费相同 API 的人决定更新它,那么我们的serverData状态变量将立即过时。

现在,你可能正在回顾GlobalStore并思考以下问题:如果selectedTheme是客户端状态,而data是服务器状态,那么isLoadingData状态变量是什么呢?

好吧,这是一个派生状态变量。这意味着它的状态将始终取决于我们当前serverData获取请求的状态。如果我们获取数据,那么isLoadingData将是true;一旦我们完成数据获取,那么isLoadingData将回到false

现在,想象一下,在你的应用程序中,每种服务器状态变量都需要一个这样的派生状态变量。我还要让你想象一个场景,在这个场景中,你需要处理获取请求失败时的错误。你可能为错误创建另一个状态变量,对吧?但你不最终会遇到和加载状态相同的问题吗?

之前提到的场景只是服务器状态带给你的应用程序挑战的冰山一角。想象一下,你的团队技术负责人有一天来到办公室告诉你,现在你需要开始缓存数据;哦,上帝,我们还没有考虑到的另一个挑战。正如你所见,服务器状态有许多挑战,在下一节中,我们将看到其中的一些。

理解服务器状态中的常见挑战

到现在为止,你可能已经意识到服务器状态带来了相当多的问题。这些挑战使得 React Query 在发布时更加突出,因为它以如此简单的方式解决了这些问题,以至于看起来太好了而不像是真的。

现在,这些挑战是什么,为什么它们大多数时候都如此复杂难以解决?

在本节中,我们将看到我们与服务器状态相关的所有常见挑战,并了解我们在 React Query 出现之前作为开发者必须自己解决的一些难题。

缓存

这可能是我们在服务器状态管理中面临的最具挑战性的问题之一。

为了提高页面性能并使你的网站更具响应性,你通常需要缓存你的数据。这意味着能够重用你之前获取的数据,以避免再次从服务器获取。

现在,你可能认为这听起来很简单,但考虑以下事项:

  • 在保持应用程序响应的同时,你需要在后台更新你的缓存。

  • 你需要能够评估你的缓存数据何时变得过时并需要更新。

  • 一旦数据有一段时间未被访问,你必须回收这些数据。

  • 在获取数据之前,你可能希望用一些模板数据初始化你的缓存。

如你所见,缓存带来了它应有的问题,想象一下你必须自己解决所有这些问题。

乐观更新

在执行变更时,你通常希望提升用户体验。变更是一个请求,它将创建或更新你的服务器状态。有时,你希望提升用户体验。我们都讨厌填写表格,然后看着加载指示器,同时我们的应用程序在后台执行变更、重新获取数据并更新用户界面。

为了提升用户体验,我们可以求助于乐观更新。

乐观更新是指在变更进行中时,我们更新我们的用户界面以显示变更完成后将如何显示,尽管那个变更尚未被确认完成。基本上,我们是乐观的,认为这些数据将改变,并在变更后成为我们期望的样子,这样我们就可以为用户节省一些时间,并给他们一个他们最终会看到的用户界面。

现在,想象一下实现这一点。在进行变更时,你需要以我们期望变更成功后的方式更新应用程序中的服务器状态。这将使 UI 对用户更加响应,他们可以更早地与之交互。变更成功后,你需要重新触发手动重新获取服务器状态,以便你实际上在应用程序中拥有更新的状态。现在,想象一个变更失败的场景。你需要手动将状态回滚到乐观更新之前的版本。

乐观更新为用户提供了一个惊人的用户体验,但管理所有成功和错误场景,以及保持服务器数据更新,可能是一件困难的事情。

去重请求

让我们描绘以下场景。

你在 UI 中有一个按钮,当用户点击时,会触发一个获取请求以部分更新你的服务器状态。在获取操作进行时,按钮被禁用。

这可能看起来没问题,一点也不麻烦,但想象一下,在你加载状态更新和你的按钮最终被禁用之前,用户可以点击按钮 10 次。你得到了什么?应用程序中针对相同数据的 10 次额外的意外请求。

这就是为什么去重请求很重要。当获取相同类型的数据时,如果我们触发了针对相同数据的多个请求,我们只想发送其中一个请求,并避免用不必要的请求污染用户的网络。

现在,想象一下你需要自己实现这一点。你需要了解应用程序中当前正在进行的所有请求。当其中一个请求与另一个请求完全匹配时,你需要取消第二个、第三个或第四个请求。

性能优化

有时,你可能需要在服务器状态中做一些额外的性能优化。以下是一些你可能需要用于特定服务器状态管理的优化模式。

  • 延迟加载:你可能只想在满足特定条件时执行一次特定的数据获取请求。

  • 无限滚动:当处理大量列表时,无限滚动是一种非常常见的模式,你只是逐渐将更多数据加载到你的服务器状态中。

  • 分页数据:为了帮助结构化大型数据集,你可以选择分页你的数据。这意味着每当用户决定从第 1 页移动到第 2 页时,你需要获取该页面对应的数据。

正如你所见,我们需要解决几个挑战,才能在我们的应用程序中拥有我们认为是处理服务器状态的最佳体验。

问题在于,作为开发者,决定自己处理这些挑战可能需要相当长的时间,而我们最终创建的代码往往容易出错。大多数情况下,这些实现最终会影响我们代码的可读性,并显著增加理解我们项目所需的复杂性。

如果我告诉你,有一种东西可以在后台为你处理所有这些挑战,同时给你一个超级干净、简单的 API,这将使你的代码更易于阅读、理解,并让你感觉自己是一位真正的服务器状态大师,你会怎么想?

如果你正在阅读这本书,那么你可能已经知道了答案。是的,我正在谈论 React Query。

因此,打包好你的服务器状态知识,准备好你的项目,因为从下一章开始,我们将改变你处理服务器状态的方式。

摘要

在本章中,我们完全理解了全局状态的概念。到现在为止,你应该能够理解为什么我们的状态经常被称为全局状态,以及如果我们不将其拆分,维护它可能会变得多么困难。

你已经学会了如何将你的状态分为客户端和服务器端状态,并理解了每种类型的状态对于你的应用的重要性,以及如何在你的代码中识别它们。

最后,你已经熟悉了服务器状态可能给你的应用带来的挑战,并理解了如果你要自己解决所有这些问题,那么你的代码复杂性将会显著增加,你可能会失去一些非常需要的睡眠时间。

第三章《React Query – 介绍、安装和配置》中,你将开始亲身体验 React Query。你将了解它是什么,以及它是如何帮助你摆脱服务器状态给应用带来的所有烦恼。你将学习如何为你的应用安装和配置它,以及如何添加专门的 React Query 开发者工具,使你的开发生活更加轻松。

第三章:React Query – 介绍、安装和配置

React Query 是一个库,旨在让 React 开发者更容易地管理他们的服务器状态。它使得开发者能够克服与服务器状态相关的所有挑战,同时使他们的应用程序更快、更容易维护,并减少代码中的许多行。

在本章中,你将了解 React Query 并了解为什么它被创建。

你还将了解 React Query 的主要概念——查询突变

一旦你了解了 React Query,我们将在我们的应用程序中安装它,并确定我们需要在代码中进行的初始配置,以便完全使用它。

在本章结束时,你将了解所有关于 React Query Devtools 的内容,以便在使用 React Query 时拥有更好的开发者体验。

在本章中,我们将涵盖以下主题:

  • 什么是 React Query?

  • 安装 React Query

  • 配置 React Query

  • 将 React Query Devtools 添加到你的应用程序

技术要求

在本章中,我们将向我们的应用程序添加 React Query v4。为此,我们需要做几件事情:

  • 你的浏览器需要与以下配置兼容:

    • Google Chrome 版本需要至少为 73

    • Mozilla Firefox 版本需要至少为 78

    • Microsoft Edge 版本需要至少为 79

    • Safari 版本需要至少为 12.0

    • Opera 版本需要至少为 53

  • 版本 16.8 之后的 React 项目

本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/State-management-with-React-Query/tree/feat/chapter_3

什么是 React Query?

React Query 是一个协议无关的钩子集合,用于在 React 中获取、缓存和更新服务器状态。

它是由 Tanner Linsley 创建的,是名为 TanStack 的一系列开源库的一部分。

默认情况下,React Query 也可以与 React Native 无缝协作,并且它是用 TypeScript 编写的,这样你可以从所有其优势中受益,例如类型缩小和类型推断。

自版本 4 以来,React Query 已嵌入到名为 TanStack Query 的一系列库中。TanStack Query 使得将 React Query 的所有惊人功能传播到其他框架和库(如 Vue、Solid 和 Svelte)成为可能。

React Query 利用查询和突变来处理你的服务器状态。在阅读最后一句话后,你可能会想知道查询和突变是什么。我将在后续章节中展示一些代码,以便你可以看到 React Query 如何处理它们,但首先,让我们了解查询和突变。

查询

查询是你向异步源发出的请求,以获取你的数据。只要你有触发数据获取请求的函数,你就可以在 React Query 中执行查询。

通过允许我们将请求包裹在返回 promise 的函数中,React Query 支持 REST、GraphQL 以及任何其他异步数据获取客户端。

在 React Query 中,useQuery 自定义钩子允许你订阅查询。

突变

突变是一种操作,允许你创建、更新或删除你的服务器状态。

与查询一样,只要你有触发突变的函数,React Query 就支持 REST、GraphQL 以及任何其他异步数据获取客户端。

在 React Query 中,useMutation 自定义钩子允许你执行突变。

React Query 如何解决我的服务器状态挑战?

如果我告诉你,前一章中提出的所有挑战都可以通过 React Query 解决,你会怎么想?

无需配置,React Query 支持以下所有令人惊叹的功能:

  • 缓存:在每次查询之后,数据将在可配置的时间内被缓存,并且可以在整个应用程序中重复使用。

  • 查询取消:你的查询可以被取消,你可以在取消后执行一个操作。

  • 乐观更新:在突变过程中,你可以轻松地更新你的状态,以便为用户提供更好的用户体验。如果突变失败,你还可以轻松地回滚到之前的状态。

  • 并行查询:如果你需要同时执行一个或多个查询,你可以轻松地做到这一点,而不会对你的缓存造成任何影响。

  • 依赖查询:有时,我们需要在另一个查询完成后执行一个查询。React Query 使这变得简单,并避免了链式 promise。

  • 分页查询:使用 React Query 可以使这种 UI 模式变得更加简单。你会发现使用分页 API、切换页面和渲染获取的数据非常简单。

  • 无限查询:React Query 使这种 UI 模式变得更加简单。你可以将无限滚动实现到你的 UI 中,并且可以信任 React Query 在获取数据时使你的生活变得更简单。

  • 滚动恢复:你是否曾经从一个页面导航出去,然后当你导航回来时,发现页面滚动到了你离开时的确切位置?这是滚动恢复,只要你的查询结果被缓存,它就会自动工作。

  • 数据重新获取:需要触发数据重新获取吗?React Query 允许你通过几乎一行代码就能做到这一点。

  • 数据预获取:有时,你可以提前识别出用户的需求和后续操作。当这种情况发生时,你可以信任 React Query 在此之前帮助你预获取那些数据并为你缓存它们。这样,你的用户体验将得到改善,并且用户会感到更加满意。

  • 跟踪网络模式和离线支持:你是否曾经遇到过用户在使用你的应用程序时丢失互联网连接的情况?别担心,因为 React Query 可以跟踪你的网络当前状态,如果查询失败是因为用户失去了连接,那么一旦网络恢复,它将重试。

看着这个列表真是太棒了,对吧?

只要有开箱即用的缓存,这绝对是一个节省时间的超级好方法,因为当处理服务器状态时,这绝对是最难实现的事情之一。

在 React Query 之前,处理我们应用程序中的服务器状态要困难得多。我们尝试过,但我们的解决方案最终变得更加复杂,代码的可维护性更低。通常,这些实现甚至会影响用户体验,因为我们的应用程序会变得不那么响应。

使用 React Query,你现在能够大大减少代码中的行数,使你的应用程序更容易阅读和简单,同时,使你的应用程序更快、更响应。

我现在不会深入更多技术细节,因为,希望在下章中,你会看到所有这些功能的工作,并开始理解为什么 React Query 使你的生活变得如此简单。

现在,让我们先在我们的应用程序中安装 React Query。

安装 React Query

现在你已经了解了 React Query,你可能正在想,“哇,我真的很需要把这个添加到我的项目中。”别再等了——这就是你需要做的来安装 React Query。

根据你的项目类型,你可以以几种方式安装 React Query。

npm

如果你正在你的项目中运行 npm,那么这是你需要做的来安装 React Query。

在你的终端中,运行以下命令:

npm i @tanstack/react-query

Yarn

如果你更喜欢 Yarn,那么这是你需要做的来安装 React Query。

在你的终端中,运行以下命令:

yarn add @tanstack/react-query

pnpm

如果你是一位新包管理器的粉丝,比如 pnpm,并且正在你的项目中使用它,那么你需要这样做来安装 React Query。

在你的终端中,运行以下命令:

pnpm add @tanstack/react-query

脚本标签

没有使用包管理器?别担心,因为你可以通过使用托管在 内容 分发网络 上的全局构建来将 React Query 添加到你的应用程序中。

内容分发网络 (CDN)

CDN 是一组地理上分布的服务器集合,它们协同工作以允许在互联网上更快地交付内容。

要将 React Query 添加到你的应用程序中,在你的 HTML 文件末尾添加以下 script 标签:

<script src="img/index.production.js"></script>

现在你应该在项目中安装了 React Query。

现在,我们需要在我们的项目中进行初始配置,以便能够使用 React Query 的所有核心功能。

配置 React Query

React Query 具有非常快速和简单的配置。这提高了开发者的体验,并可以让你尽快开始将你的服务器状态迁移到 React Query。

要将 React Query 添加到您的应用程序中,您只需要了解两件事:

  • QueryClient

  • QueryClientProvider

QueryClient

如您现在所应知道的,缓存是 React Query 为开发者简化的重要事情之一。在 React Query 中,有两种机制用于处理此缓存,称为 QueryCacheMutationCache

QueryCache 负责存储与您的查询相关的所有数据。这可以是您的查询数据以及其当前状态。

MutationCache 负责存储与您的突变相关的所有数据。这可以是您的突变数据以及其当前状态。

为了让开发者更容易从这两个缓存中抽象出来,React Query 创建了 QueryClient。这是开发者与缓存之间的接口。

当您使用 React Query 设置应用程序时,您应该做的第一件事是创建一个 QueryClient 实例。为此,您需要从 @tanstack/react-query 包中导入它并实例化它:

import {
 QueryClient,
} from '@tanstack/react-query'
const queryClient = new QueryClient()

在前面的代码片段中,我们创建了一个新的 QueryClient 对象。由于我们在实例化对象时没有传递任何参数,因此 QueryClient 将假定所有默认值。

在创建我们的 QueryClient 时,我们可以作为参数发送四个选项。它们如下所示:

  • queryCache:此客户端将在整个应用程序中使用查询缓存。

  • mutationCache:此客户端将在整个应用程序中使用突变缓存。

  • logger:此客户端将使用它来显示错误、警告以及调试时有用的信息。如果没有指定任何内容,React Query 将使用控制台对象。

  • defaultOptions:所有查询和突变将在整个应用程序中使用的默认选项。

现在,您可能想知道何时应该手动设置这些参数而不是使用默认值。以下的小节将告诉您何时这样做。

QueryCache 和 MutationCache

这里有一个小小的预告,希望您在接下来的章节中能够复习并更好地理解,但了解何时手动配置 QueryCacheMutationCache 是非常重要的——所有查询和突变都可以在出现错误或执行成功时执行一些代码。这些代码由 onSuccessonError 函数表示。此外,在突变的情况下,您还可以在突变执行之前执行一些代码。在这种情况下,表示这个功能的函数被称为 onMutate

QueryCache 的情况下,它看起来是这样的:

import { QueryCache } from '@tanstack/react-query'
const queryCache = new QueryCache({
 onError: error => {
  // do something on error
 },
 onSuccess: data => {
  // do something on success
 }
})

在解释前面的代码片段之前,让我们先看看与之非常相似的 MutationCache

import { MutationCache } from '@tanstack/react-query'
const mutationCache = new MutationCache({
 onError: error => {
  // do something on error
 },
 onSuccess: data => {
  // do something on success
 },
 onMutate: newData => {
  // do something before the mutation
 },
})

如您所见,这两个代码片段非常相似,只是在 MutationCache 上的 onMutate 函数有所不同。

默认情况下,这些函数没有任何行为,但如果出于某种原因,你打算在执行突变或查询时始终执行某些操作,那么你可以在实例化缓存对象时,在相应对象的相应函数中进行此配置。

然后,你可以在实例化QueryClient时将此对象发送给它:

const queryClient = new QueryClient({
 mutationCache,
 queryCache
})

在前面的代码片段中,我们使用我们的自定义MutationCacheQueryCache函数实例化了一个新的QueryClient

Logger

你在你的项目中是否在console对象之外使用logger?那么,你可能想在QueryClient中配置它。

这里你需要做的是:

const logger = {
   log: (...args) => {
     // here you call your custom log function
   },
   warn: (...args) => {
     // here you call your custom warn function
   },
   error: (...args) => {
     // here you call your custom error function
   },
 };

在前面的代码片段中,我们创建了一个logger对象。这个对象有三个函数,React Query 将在需要记录错误、警告错误或显示错误时调用这些函数。你可以覆盖这些函数并添加你自己的自定义记录器。

然后,你所需要做的就是在你实例化QueryClient时传递这个logger对象:

const queryClient = new QueryClient({
 logger
})

在前面的代码片段中,我们使用我们的自定义记录器实例化了一个新的QueryClient

defaultOptions

有一些选项被用作你在整个应用程序中执行的所有突变或查询的默认值。defaultOptions允许你覆盖这些默认值。有很多默认值,我会避免展示所有这些,以免泄露下一章的内容,但请放心——在适当的时候,我会对这些选项进行回调。

这里是如何覆盖你的defaultOptions

const defaultOptions = {
   queries: {
     staleTime: Infinity,
   },
 };

在前面的代码片段中,我们创建了一个defaultOptions对象,并在其中创建了一个queries对象。在这个queries对象内部,我们指定了所有查询的staleTime都将设置为Infinity。再次提醒,不要担心现在还没有对这个定义,你将在下一章中理解它。

一旦完成这个设置,你所需要做的就是在你实例化QueryClient时传递这个defaultOptions对象,这样所有的查询都将具有staleTime属性并设置为Infinity

这里是如何操作的:

const queryClient = new QueryClient({
 defaultOptions
})

在前面的代码片段中,我们使用我们的自定义defaultOptions对象实例化了一个新的QueryClient

好的,所以现在你应该已经了解了QueryClient及其在 React Query 中作为大脑的角色。

所以,你可能正在想,考虑到 React Query 是基于钩子进行查询和突变的,我们是否需要始终将我们的QueryClient传递给所有的钩子?

想象一下如果是这种情况!在我们使用第二个或第三个钩子之前,我们都会对应用程序中的所有属性钻探感到厌烦。

让我们看看 React Query 通过引入QueryClientProvider如何帮助我们节省时间。

QueryClientProvider

为了让每个开发者更容易地共享我们的 QueryClient,React Query 采用了我们在 第一章 中学到的某种方法,那就是 React Context。通过创建其自定义提供者 QueryClientProvider,React Query 允许您与它自动提供的所有自定义钩子共享 QueryClient

下面的代码片段展示了如何使用 React Query 的 QueryClientProvider

import {
 QueryClient,
 QueryClientProvider,
} from '@tanstack/react-query'
// Create a client
const queryClient = new QueryClient()
const App = () => {
 return (
   <QueryClientProvider client={queryClient}>
     <Counter />
   </QueryClientProvider>
 )
}

正如您在前面的代码片段中所看到的,您需要做的就是从 @tanstack/react-query 包中导入您的 QueryClientProvider,用它包裹您的主体组件,并将其作为属性传递给 queryClient

您的应用程序现在已准备好开始使用 React Query。

现在,让我们看看如何添加和使用 React Query 专用的开发者工具。

添加 React Query Devtools

在调试我们的应用程序时,我们经常发现自己在想,如果有一种方法可以可视化应用程序内部发生的事情,那会多么美妙。好吧,有了 React Query,您不必担心,因为它有自己的开发者工具,或者称为 devtools。

React Query Devtools 允许您查看和理解所有查询和突变当前的状态。这将为您节省大量调试时间,并避免在所有代码中污染不必要的日志函数,即使只是暂时性的。

根据项目类型,您可以通过几种方式安装 React Query Devtools:

  • 如果您在项目中运行 npm,请运行以下命令:

    npm i @tanstack/react-query-devtools
    
  • 如果您正在使用 Yarn,请运行以下命令:

    yarn add @tanstack/react-query-devtools
    
  • 如果您正在使用 pnpm,请运行以下命令:

    pnpm add @tanstack/react-query-devtools
    

现在,您应该在您的应用程序中安装了 React Query Devtools。现在,让我们看看如何将它们添加到我们的代码中。

使用 Devtools 有两种方式。它们是浮动模式和嵌入式模式。

浮动模式

浮动模式将在屏幕角落浮动显示 React Query 标志。通过点击它,您可以切换 Devtools 的开启或关闭。

将显示在您屏幕角落的标志如下:

图片 3.1

图 3.1 – React Query Devtools 的标志

一旦切换,您将看到 Devtools:

图片 3.2

图 3.2 – React Query Devtools 的浮动模式

Devtools 将在您的 DOM 树中作为单独的 HTML 元素渲染。

图片 3.3

图 3.3 – React Query Devtools 的浮动模式在 DOM 上的显示

要将 Devtools 以浮动模式添加到您的应用程序中,您需要导入它:

import { ReactQueryDevtools } from '@tanstack/
  react-query-devtools'

导入后,只需将其添加到尽可能靠近您的 QueryClientProvider 的位置:

   <QueryClientProvider client={queryClient}>
     <ReactQueryDevtools initialIsOpen={false} />
     <Counter />
   </QueryClientProvider>

嵌入式模式

嵌入式模式会将 Devtools 嵌入为应用程序中的常规组件。

这是它在您的应用程序中的样子:

图片 3.4

图 3.4 – React Query Devtools 的嵌入式模式

如果您查看您的 DOM 树,您将看到 Devtools 被像常规组件一样渲染。

图片 3.5

图 3.5 – React Query Devtools 的嵌入式模式在 DOM 上的显示

要在你的应用程序中使用嵌入式模式的 Devtools,你需要导入它:

import { ReactQueryDevtoolsPanel } from '@tanstack/
  react-query-devtools'

一旦它们被导入,只需将它们添加到尽可能靠近你的 QueryClientProvider 的位置:

   <QueryClientProvider client={queryClient}>
     <ReactQueryDevtoolsPanel />
     <Counter />
   </QueryClientProvider>

默认情况下,Devtools 不包含在生产构建中。尽管如此,你可能会想在生产环境中加载它们以帮助调试某些问题。在下一节中,我们将看到如何做到这一点。

启用生产构建中的 Devtools

如果你决定在生产环境中加载 Devtools,你必须延迟加载它,而不是动态加载。这很重要,可以帮助减少你的应用程序包大小。同样重要的是懒加载 Devtools,因为当我们在生产环境中使用我们的应用程序时,我们可能永远不想使用它,所以我们想避免在我们的构建中添加最终根本不会使用的东西。在 React 中,我们可以使用 React.lazy 来懒加载组件。

这是我们可以使用 React.lazy 导入 Devtools 的方法:

const ReactQueryDevtoolsProduction = React.lazy(() =>
  import('@tanstack/react-query-devtools/build/lib/
    index.prod.js').then(
    (d) => ({
      default: d.ReactQueryDevtools,
    }),
  ),
)

前面的代码片段包裹了一个 React.lazy 并将承诺的返回值赋给 ReactQueryDevtoolsProduction,这样我们就可以在我们的生产环境中懒加载它,而不会增加我们的包大小。

什么是动态导入?

动态导入允许你从代码中的任何位置异步加载一个模块。此导入将返回一个承诺,当承诺被满足时,返回一个包含模块导出的对象。

前面的代码片段应该适用于所有打包器。如果你使用的是一个支持包导出的更现代的打包器,那么你可以像这样动态导入你的模块:

const ReactQueryDevtoolsProduction = React.lazy(() =>
  import('@tanstack/react-query-devtools/production').then(
    (d) => ({
      default: d.ReactQueryDevtools,
    }),
  ),
)

在这个代码片段中,我们将导入模块的路径从我们将要导入的路径更改为一个可以与更现代的打包器一起工作的路径。

当使用 React.lazy 并尝试渲染我们刚刚懒加载的组件时,React 要求该组件应该被一个 Suspense 组件包裹。这在我们要在懒加载的组件待定期间显示回退内容的情况下非常重要。

什么是悬念?

Suspense 允许你在组件内部尚未准备好渲染时,在你的 UI 中显示加载指示。

让我们看看我们需要做什么来加载我们的 ReactQueryDevtoolsProduction 组件:

<React.Suspense fallback={null}>
  <ReactQueryDevtoolsProduction />
</React.Suspense>

如代码片段所示,我们用 Suspense 包裹了 ReactQueryDevtoolsProduction 组件,以便它可以被懒加载。你还可以看到我们没有提供任何回退,因为我们正在尝试加载的是 Devtools,我们不需要在模块加载期间添加任何待定状态。

现在,我们不想在渲染我们的组件时自动加载 Devtools。我们想要的只是在我们的应用程序中切换它们的方式。

由于这是一个生产构建,我们不希望在其中有可能会让用户困惑的按钮。因此,一种潜在的处理方式是在我们的 window 对象内部创建一个名为 toggleDevtools 的函数。

这是 React Query 文档建议我们这样做的方式:

  const [showDevtools, setShowDevtools] = React.useState
    (false)
  React.useEffect(() => {
    window.toggleDevtools = () => setShowDevtools
      ((previousState) => !previousState)
  }, [])
  return (
    …
      {showDevtools && (
        <React.Suspense fallback={null}>
          <ReactQueryDevtoolsProduction />
        </React.Suspense>
      )}
    …
  );

在前面的代码片段中,我们做了以下操作:

  1. 创建一个状态变量来保存 Devtools 的当前状态。这个状态变量在用户打开或关闭 Devtools 时更新。

  2. window上运行一个效果,将切换函数分配给我们的window

  3. 在我们的返回中,当我们的showDevtools被切换为开启时,由于我们正在懒加载我们的ReactQueryDevtoolsProduction组件,我们需要用Suspense包裹它,以便能够渲染它。

到目前为止,你已经拥有了在应用程序中使用 React Query 所需的一切。

摘要

在本章中,我们学习了 TanStack Query 以及 React Query 如何融入其中。到现在,你应该能够识别 React Query 使服务器状态管理变得更容易的主要方式,以及它是如何使用查询和突变的。

你学习了QueryClientQueryClientProvider,并了解了它们对于在应用程序中运行 React Query 是基本性的。你还学习了如果你需要,你可以如何自定义自己的QueryClient

最后,你将遇到 React Query Devtools,并学习如何在你的项目中配置它。现在,你也能够在需要做额外调试的特殊场景中将它加载到生产环境中。

第四章使用 React Query 获取数据中,你将了解你的最佳查询助手——useQuery自定义钩子。你将理解它是如何工作的,如何使用它,以及它如何缓存数据。你还将了解你可以触发查询重新获取查询的方式,以及如何构建依赖查询。

第二部分:使用 React Query 管理服务器状态

当处理服务器状态时,许多挑战都与我们从其中读取的方式有关。从缓存到分页,我们将了解 React Query 的自定义钩子useQuery是如何使这项工作变得容易,同时提供令人惊叹的开发者和用户体验。

以及我们读取服务器状态时的挑战,创建、更新和删除它又带来了一组新的挑战。幸运的是,React Query 还有一个名为useMutation的自定义钩子来提供帮助。

在理解了 React Query 的支柱之后,你可能想知道流行的服务器端框架如 Next.js 和 Remix 是否允许你使用 React Query。剧透一下——它们确实可以,你在这里将学习如何使用。

为了总结并确保你晚上能睡得香,你将学习一套你可以用来测试 React Query 的食谱,通过利用 Mock Service Worker 和 React Testing Library 来使用组件和自定义钩子。

这一部分包括以下章节:

  • 第四章, 使用 React Query 获取数据

  • 第五章, 更多数据获取挑战

  • 第六章, 使用 React Query 执行数据突变

  • 第七章, 使用 Next.js 或 Remix 进行服务器端渲染

  • 第八章, 测试 React Query 钩子和组件

第四章:使用 React Query 获取数据

React Query 通过利用其自定义钩子之一useQuery,允许你获取、缓存和处理你的服务器状态。为了你的数据能够被缓存,React Query 有一个称为查询键的概念。结合查询键和一些严格的默认值,React Query 将你的服务器状态管理提升到新的水平。

在本章中,你将了解useQuery钩子,并理解 React Query 是如何让你获取和缓存数据的。在这个过程中,你将了解所有查询中使用的默认值。你还将了解一些可以用来使你的useQuery体验更好的选项。

在熟悉了useQuery之后,你就可以开始在特定场景下使用它来重新获取你的查询。你还可以利用useQuery的一些额外属性来获取相互依赖的查询。

在本章结束时,我们将回顾一个代码文件,以回顾本章所学的内容。

在本章中,我们将涵盖以下主题:

  • useQuery是什么以及它是如何工作的?

  • 使用useQuery重新获取数据

  • 使用useQuery获取依赖查询

技术要求

本章的所有代码示例都可以在 GitHub 上找到:github.com/PacktPublishing/State-management-with-React-Query/tree/feat/chapter_4

useQuery是什么以及它是如何工作的?

如你在上一章所学,查询是你向异步源发送的请求以获取数据。

在 React Query 文档中,查询也被定义为以下方式:

查询是对异步数据源的声明性依赖,它与一个唯一键相关联。

(tanstack.com/query/v4/docs/guides/queries)

在掌握了这个概念之后,你现在就可以理解 React Query 是如何利用其自定义钩子useQuery来让你订阅查询的。

要使用useQuery自定义钩子,你必须像这样导入它:

import { useQuery } from "@tanstack/react-query";

下面是useQuery的语法:

const values = useQuery({
   queryKey: <insertQueryKey>,
   queryFn: <insertQueryFunction>,
 });

如你所见,useQuery钩子只需要两个参数即可工作:

  • 查询键:用于标识你的查询的唯一键

  • 查询函数:一个返回 Promise 的函数

什么是查询键?

查询键是 React Query 用来标识你的查询的唯一值。它还通过使用查询键,React Query 在QueryCache中缓存你的数据。查询键还允许你手动与查询缓存进行交互。

查询键需要是一个数组,它可以包含一个字符串或一系列其他值,如对象。重要的是,这个查询键数组中的值必须是可序列化的。

在 React Query v4 之前,查询键不一定需要是一个数组。它可以是单个字符串,因为 React Query 会将其内部转换为数组。所以,如果您在网上找到一些不使用数组作为查询键的示例,请不要觉得奇怪。

这里有一些有效的查询键示例:

useQuery({ queryKey: ['users']  })
useQuery({ queryKey: ['users', 10] })
useQuery({ queryKey: ['users', 10, { isVisible: true }] })
useQuery({ queryKey: ['users', page, filters] })

如您所见,只要是一个数组,查询键就是有效的。

作为良好的实践,为了使您的查询键在阅读多个useQuery钩子时更加独特和易于识别,您应该将查询的所有依赖项作为查询键的一部分添加。将其视为与您的useEffect钩子上的依赖项数组相同的模型。这对于阅读目的很有帮助,因为查询键还允许 React Query 在查询的依赖项发生变化时自动重新获取查询。

需要记住的一点是查询键是确定性散列的。这意味着数组内部项的顺序很重要。

这里有一些查询,当它们的查询键被确定性散列时,它们是同一个查询:

useQuery({ queryKey: ['users', 10, { page, filters }] })
useQuery({ queryKey: ['users', 10, { filters, page }] })
useQuery({ queryKey: ['users', 10, { page, random:
  undefined, filters }] })

所有这些示例都是同一个查询——在三个示例中,查询键中数组的顺序保持不变。

现在,您可能想知道这是如何可能的,考虑到对象内部的面页和过滤器每次都会改变位置,在最后一个示例中还有一个名为random的第三个属性。这是真的,但它们仍然在对象内部,而这个对象在查询键数组内部的位置没有改变。此外,random属性是未定义的,所以在散列对象时,它被排除。

现在,让我们看看一些查询,当它们的查询键被确定性散列时,它们不是同一个查询:

useQuery({ queryKey: ['users', 10, undefined, { page,
  filters }] })
useQuery({ queryKey: ['users', { page, filters }, 10] })
useQuery({ queryKey: ['users', 10, { page, filters }] })

所有这些示例都代表不同的查询,因为当查询键是确定性散列时,这些示例最终会变成完全不同的查询。您可能想知道为什么第一个示例与最后一个示例不同。不应该像从{ queryKey: ['users', 10, { page, random: undefined, filters }] })对象中消失的那样,undefined值消失吗?

不,因为在当前场景中,它不在一个对象内部,并且顺序很重要。当它被散列时,这个未定义的值将在散列键内部被转换为一个 null 值。

现在您已经熟悉了查询键,您可以了解更多关于查询函数的内容。

查询函数是什么?

查询函数是一个返回 promise 的函数。这个返回的 promise 将解析并返回数据,或者抛出一个错误。

因为查询函数只需要返回一个 promise,这使得 React Query 变得更加强大,因为查询函数可以支持任何能够执行异步数据获取的客户端。这意味着RESTGraphQL都得到了支持,所以如果您愿意,您可以同时拥有这两种选项。

现在,让我们看看一个使用 GraphQL 的查询函数示例,以及一个使用 REST 的查询函数示例:

GraphQL

import { useQuery } from "@tanstack/react-query";
import { request, gql } from "graphql-request";
const customQuery = gql`
  query {
    posts {
      data {
        id
        title
      }
    }
  }
`;
const fetchGQL = async () => {
  const endpoint = <add_endpoint_here>
  const {
    posts: { data },
  } = await request(endpoint, customQuery);
  return data;
};
 …
useQuery({
queryKey: ["posts"],
queryFn: fetchGQL
});

在前面的代码片段中,我们可以看到一个使用 React Query 和 GraphQL 的例子。这是我们正在做的事情:

  1. 我们首先创建我们的 GraphQL 查询,并将其分配给我们的customQuery变量。

  2. 然后,我们创建fetchGQL函数,它将成为我们的查询函数。

  3. 在我们的useQuery钩子中,我们将相应的查询键传递给钩子,并将我们的fetchGQL函数作为查询函数。

现在,让我们看看如何使用 REST 来完成这个操作:

REST

import axios from "axios";
const fetchData = async () => {
  const { data } = await axios.get(
    `https://danieljcafonso.builtwithdark.com/
      react-query-api`
  );
  return data;
};
…
useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });

在前面的代码片段中,我们可以看到一个使用 React Query 和 REST 的例子。这是我们正在做的事情:

  1. 我们首先创建fetchData函数,它将成为我们的查询函数。

  2. 在我们的useQuery钩子中,我们将相应的查询键传递给钩子,并将我们的fetchData函数作为查询函数。

这些例子让 React Query 更加闪耀,因为只要你有能够执行异步数据获取的客户端,你就可以在查询函数中使用这个客户端。如前所述,为了确保 React Query 能够正确处理你的错误场景,在使用这些客户端时,我们需要检查的一件事是,当你的请求失败时,它们是否会自动抛出错误。如果它们不会抛出错误,你必须自己抛出错误。

这是在使用fetch的查询函数中这样做的方法:

const fetchDataWithFetch = async () => {
  const response = await fetch('https://danieljcafonso.
    builtwithdark.com/react-query-api')
  if (!response.ok) throw new Error('Something failed in
    your request')
  return response.json()
}

在前面的代码片段中,使用fetch执行请求后,我们检查我们的响应是否有效。如果不是,我们抛出一个错误。如果一切正常,我们返回响应数据。

当你持续创建查询和构建查询函数时,最终会想到的一点是,将查询键传递给查询函数会很有帮助。毕竟,如果查询键代表查询的依赖项,那么在查询函数中可能需要它们是有意义的。

你可以这样操作,有两种模式可以这样做:

  • 内联函数

  • 查询函数上下文

内联函数

当你的查询键中不需要传递许多参数到查询函数时,你可以利用这个模式。通过编写内联函数,你可以提供对当前作用域中变量的访问,并将它们传递到查询函数。

这里是这个模式的一个例子:

const fetchData = async (someVariable) => {
 const { data } = await axios.get(
   `https://danieljcafonso.builtwithdark.com/
     react-query-api/${someVariable}`
 );
 return data;
};
…
useQuery({
   queryKey: ["api", someVariable],
   queryFn: () => fetchData(someVariable),
 });

在前面的代码片段中,我们首先创建一个名为fetchData的函数,它将接收一个名为someVariable的参数。这个参数随后被用来补充用于获取数据的 URL。当我们到达useQuery声明时,由于我们需要将someVariable变量用作查询的依赖项,所以我们将其包含在查询键中。最后,在查询函数中,我们创建一个内联函数,该函数将调用fetchData并传递我们的someVariable值。

如你所见,当我们没有很多参数时,这种模式非常出色。现在,考虑一下这种情况:你的查询键最终有 12 个参数,并且它们都在查询函数内部被需要。这并不是一个坏习惯,但它会稍微影响你的代码可读性。为了避免这些情况,你可以求助于 QueryFunctionContext 对象。

QueryFunctionContext

每次调用查询函数时,React Query 会自动将你的查询键作为 QueryFunctionContext 对象传递给查询函数。

这里是使用 QueryFunctionContext 模式的一个例子:

const fetchData = async ({ queryKey }) => {
 const [_queryKeyIdentifier, someVariable] = queryKey;
 const { data } = await axios.get(
   `https://danieljcafonso.builtwithdark.com/
     react-query-api/${someVariable}`
 );
 return data;
};
useQuery({
   queryKey: ["api", someVariable],
   queryFn: fetchData,
 });

在前面的代码片段中,我们首先创建我们的 fetchData 函数。这个函数将接收 QueryFunctionContext 作为参数,因此从这个对象中,我们可以立即解构 queryKey。正如你在 什么是查询键? 部分所知,查询键是一个数组,因此我们传递给函数的参数的顺序对我们传递给查询键的参数顺序很重要。在这个例子中,我们需要 someVariable 变量,它是作为我们数组的第二个元素传递的,因此我们解构我们的数组以获取第二个元素。然后我们使用 someVariable 来补充用于获取数据的 URL。当我们到达 useQuery 声明时,由于我们需要将 someVariable 变量用作查询的依赖项,我们将其包含在查询键中。由于它包含在查询键中,它将自动发送到我们的查询函数。

这种模式减少了创建内联函数的需求,并强制要求将你的查询的所有依赖项添加到查询键中。这种模式可能的一个缺点是,当有这么多参数时,你将不得不记住它们在查询键中添加的顺序,以便在查询函数中使用它们。解决这个问题的方法之一是发送一个包含你查询函数中所需的所有参数的对象。这样,你就无需记住数组元素的顺序。

这就是你可以这样做的方式:

useQuery({
   queryKey: [{queryIdentifier: "api", someVariable}],
   queryFn: fetchData,
 });

通过将一个对象作为你的查询键传递,该对象将被作为 QueryFunctionContext 对象发送到你的查询函数。

然后,在你的函数中,你只需要这样做:

const fetchData = async ({ queryKey }) => {
  const { someVariable } = queryKey[0];
…
};

在前面的代码片段中,我们从 QueryFunctionContext 对象中解构我们的 queryKey。然后,由于我们的对象将是查询键的第一个位置,我们可以从那里解构我们需要的值。

现在你已经理解了每个 useQuery 钩子所需的两个选项,我们可以开始查看它返回的内容。

useQuery 返回什么?

当使用 useQuery 钩子时,它返回几个值。要访问这些值,你只需将钩子的返回值分配给一个变量或从钩子的返回值中解构值即可。

你可以这样做:

const values = useQuery(...);
const { data, error, status, fetchStatus }= useQuery(...);

在这个代码片段中,我们可以看到访问 useQuery 钩子返回值的两种不同方式。

在本节中,我们将回顾 useQuery 钩子的以下返回值:

  • data

  • error

  • status

  • fetchStatus

data

这个变量是查询函数返回的最后成功解析的数据。

这就是你可以使用data变量的方式:

const App = () => {
  const { data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  return (
    <div>
       {data ? data.hello : null}
    </div>
  );
};

在这个代码片段中,我们做了以下操作:

  1. 我们从useQuery钩子中解构data变量。

  2. 在我们的返回中,我们检查是否已经从我们的查询中获取了数据。当我们这样做时,我们将渲染它。

当查询最初执行时,这些数据将是未定义的。一旦它执行完成并且查询函数成功解析了你的数据,我们就能访问这些数据。如果由于某种原因,我们的查询函数的 promise 被拒绝,那么我们可以使用下一个变量:error

error

error变量让你能够访问查询函数失败后返回的错误对象。

这就是你可以使用error变量的方式:

const App = () => {
  const { error } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  return (
    <div>
       {error ? error.message : null}
    </div>
  );
};

在前面的代码片段中,我们做了以下操作:

  1. 我们从useQuery钩子中解构error变量。

  2. 在我们的返回中,我们检查是否有任何错误。如果有,我们渲染error信息。

当查询最初执行时,error值将是 null。如果由于某种原因,查询函数拒绝并抛出错误,那么这个错误将被分配给我们的error变量。

dataerror的示例中,我们都检查了它们是否已定义,这样我们就可以让我们的应用程序用户知道我们查询的当前状态。为了使这更容易,并帮助你为应用程序创建更好的用户体验,添加了status变量。

status

在执行查询时,查询可以经过几个状态。这些状态帮助你向用户提供更多的反馈。为了让你知道查询的当前状态,创建了status变量。

这里是status变量可能具有的状态:

  • loading:没有查询尝试完成,并且还没有缓存的数据。

  • error:在执行查询时出现了错误。每当这是状态时,error属性将接收查询函数返回的错误。

  • success:你的查询成功并且返回了数据。每当这是状态时,data属性将接收查询函数的成功数据。

这就是你可以使用status变量的方式:

const App = () => {
  const { status, error, data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  if(status === "loading") {
    return <div>Loading...</div>
  }
  if(status === "error") {
    return <div>There was an unexpected error:
      {error.message}</div>
  }
  return (
    <div>
       {data.hello}
    </div>
  );
};

在前面的代码片段中,我们正在利用status变量为我们的用户提供更好的用户体验。这是我们在做的事情:

  1. 我们首先从useQuery钩子中解构status变量。

  2. 我们检查status是否为加载状态。这意味着我们还没有任何数据,并且我们的查询已经完成。如果情况如此,我们将渲染一个加载指示器。

  3. 如果我们的status不是加载状态,我们检查查询过程中是否出现了任何错误。如果我们的status等于error,那么我们需要解构我们的error变量并显示错误信息。

  4. 最后,如果我们的status也不是错误状态,那么我们可以安全地假设我们的status等于成功;因此,我们应该有查询函数返回的数据的data变量,并且我们可以将其显示给用户。

现在,你已经知道了如何使用status变量。为了方便,React Query 还引入了一些布尔变体来帮助我们识别每个状态。它们如下所示:

  • isLoading:你的status变量处于加载状态

  • isError:你的status变量处于错误状态

  • isSuccess:你的status变量处于成功状态

让我们重写我们之前的代码片段,利用我们的status布尔变体:

const App = () => {
  const {  isLoading, isError, error, data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  if(isLoading) {
    return <div>Loading...</div>
  }
  if(isError) {
    return <div>There was an unexpected error:
      {error.message}</div>
  }
  return (
    <div>
       {data.hello}
    </div>
  );
};

如你所见,代码是相似的。我们只需要在我们的解构中将status变量替换为isLoadingisError,然后在相应的状态检查中使用isLoadingisError变量。

现在,status变量为你提供了关于你的查询数据的信息。然而,这并不是 React Query 拥有的唯一状态变量。在下一节中,你将介绍fetchStatus

fetchStatus

在 React Query v3 中,他们发现当处理用户可能离线的情况时存在一个问题。如果用户触发了查询,但在请求过程中由于某种原因失去了连接,status变量将保持在加载状态,直到用户重新获得连接并且查询自动重试。

为了处理这类问题,在 React Query v4 中,他们引入了一个新的属性,称为networkMode。这个属性可以有三种状态,但默认情况下将使用在线状态。好事是这种模式允许你使用fetchStatus变量。

fetchStatus变量为你提供了关于你的查询函数的信息。

这里是这个变量可能具有的状态:

  • fetching:你的查询函数目前正在执行。这意味着它目前正在获取数据。

  • paused:你的查询想要获取数据,但由于失去了连接,它现在已经停止执行。这意味着它目前处于暂停状态。

  • idle:查询目前没有任何操作。这意味着它目前处于空闲状态。

现在,让我们学习如何使用fetchStatus变量:

const App = () => {
  const {  fetchStatus, data } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  if(fetchStatus === "paused") {
    return <div>Waiting for your connection to return…
      </div>
  }
  if(fetchStatus === "fetching") {
    return <div>Fetching…</div>
  }
  return (
    <div>
       {data.hello}
    </div>
  );
};

在前面的代码片段中,我们正在利用fetchStatus变量为我们的用户提供更好的用户体验。我们正在做的是:

  1. 我们首先从useQuery钩子的返回值中解构出fetchStatus变量。

  2. 我们接着检查我们的fetchStatus当前状态是否为暂停。如果是true,那么现在没有网络连接,因此我们让我们的用户知道。

  3. 如果之前的If 检查false,那么我们可以验证我们的fetchStatus当前状态是否为获取。如果之前的If 检查true,那么现在查询函数正在运行,因此我们让我们的用户知道。

  4. 如果我们不是在获取数据,那么我们可以假设我们的查询函数的fetchStatus是空闲的;因此,它已经完成了获取,所以我们应该有返回的数据。

现在,你已经知道了如何使用fetchStatus变量。就像status变量一样,React Query 也引入了一些布尔变体来帮助我们识别这两种状态。它们如下所示:

  • isFetching:您的 fetchStatus 变量处于获取状态

  • isPaused:您的 fetchStatus 变量处于暂停状态

让我们利用我们的 fetchStatus 布尔变体重写之前的片段:

const App = () => {
  const { isFetching, isPaused, data } = useQuery({
    queryKey" [""pi"],
    queryFn: fetchData,
  });
  if(isPaused) {
    return <div>Waiting for your connection to return...
      </div>
  }
  if(isFetching) {
    return <div>Fetching...</div>
  }
  return (
    <div>
       {data.hello}
    </div>
  );
};

如您从片段中看到的,代码相当相似。我们只需将我们的 fetchStatus 变量替换为解构中的 isFetchingisPaused,然后在相应的 fetchStatus 检查中使用这些 isFetchingisPaused 变量。

既然我们已经了解了 useQuery 钩子返回的值,让我们看看我们如何使用一些选项来定制相同的钩子。

常用选项解释

当使用 useQuery 钩子时,可以传递比查询键和查询函数更多的选项。这些选项帮助您创建更好的开发者体验,以及更好的用户体验。

在本节中,我们将探讨一些更常见且非常重要的选项,您需要了解。

下面是我们要介绍的一些选项:

  • staleTime

  • cacheTime

  • retry

  • retryDelay

  • 启用

  • onSuccess

  • onError

staleTime

staleTime 选项是查询数据不再被认为是 新鲜 的毫秒数。当设置的时间过去后,查询会被称为 过时

当查询处于 新鲜 状态时,它将从缓存中拉取,而不会触发更新缓存的新请求。当查询被标记为 过时 时,数据仍然会从缓存中拉取,但可以触发查询的自动重新获取。

默认情况下,所有查询都使用设置为 0staleTime。这意味着所有缓存数据默认都会被认为是 过时 的。

这是我们如何配置 staleTime 的方法:

useQuery({
  staleTime: 60000,
});

在这个片段中,我们定义这个钩子的查询数据在一分钟内被认为是 新鲜 的。

cacheTime

cacheTime 选项是缓存中不活跃数据在内存中保持的时间(以毫秒为单位)。一旦这个时间过去,数据将被垃圾回收。

默认情况下,当没有 useQuery 钩子的活动实例时,查询会被标记为不活跃。当这种情况发生时,这个查询数据将保留在缓存中 5 分钟。在这 5 分钟后,这些数据将被垃圾回收。

这就是如何使用 cacheTime 选项:

useQuery({
  cacheTime: 60000,
});

在片段中,我们定义了在查询不活跃 1 分钟后,数据将被垃圾回收。

重试

retry 选项是一个值,表示查询失败时是否会重试。当 true 时,它会重试直到成功。当 false 时,它不会重试。

这个属性也可以是一个数字。当它是一个数字时,查询将重试指定次数。

默认情况下,所有失败的查询都会重试三次。

这就是如何使用 retry 选项:

useQuery({
  retry: false,
});

在这个片段中,我们将 retry 选项设置为 false。这意味着当查询失败时,这个钩子不会尝试重新获取数据。

我们也可以这样配置 retry 选项:

useQuery({
  retry: 1,
});

在这个片段中,我们将数字 1 设置为 retry 选项。这意味着如果这个钩子无法获取查询,它将只重试请求一次。

retryDelay

retryDelay 选项是在下一次重试尝试之前应用的延迟时间(以毫秒为单位)。

默认情况下,React Query 使用指数退避延迟算法来定义重试之间的时间间隔。

这是使用 retryDelay 选项的方法:

useQuery({
  retryDelay: (attempt) => attempt * 2000,
});

在这个片段中,我们定义了一个线性退避函数作为我们的 retryDelay 选项。每次重试时,这个函数都会接收到尝试次数并将其乘以 2000。这意味着每次重试之间的时间间隔将增加 2 秒。

enabled

enabled 选项是一个布尔值,表示您的查询何时可以运行或不能运行。

默认情况下,此值是 true,因此所有查询都被启用。

这是使用 enabled 选项的方法:

useQuery({
  enabled: arrayVariable.length > 0
});

在这个片段中,我们将表达式评估的返回值分配给 enabled 选项。这意味着只要 arrayVariable 的长度大于 0,这个查询就会执行。

onSuccess

onSuccess 选项是一个函数,当您的查询在获取过程中成功时将被触发。

这是使用 onSuccess 选项的方法:

useQuery({
  onSuccess: (data) => console.log("query was successful",
    data),
});

在这个片段中,我们将一个箭头函数传递给我们的 onSuccess 选项。当我们的查询成功获取数据时,这个函数将使用我们的 data 作为参数被调用。然后我们使用这个 data 来在我们的 console 中进行日志记录。

onError

当您的查询在获取过程中失败时,onError 选项是一个将被触发的函数。

这是使用 onError 选项的方法:

useQuery({
  onError: (error) => console.log("query was unsuccessful",
    error.message),
});

在这个片段中,我们将一个箭头函数传递给我们的 onError 选项。当查询失败时,这个函数将使用 thrown 错误作为参数被调用。然后我们在我们的 console 中记录错误。

如您所见,useQuery 钩子支持很多选项,而之前展示的只是冰山一角。在接下来的章节中,您将了解到更多,所以请做好准备!

您现在熟悉了 useQuery 钩子,应该能够使用它来开始获取您的服务器状态数据。现在,让我们看看一些模式和方式,我们可以使用这个钩子来处理一些常见的服务器状态挑战。

使用 useQuery 重新获取数据

重新获取数据是管理我们的服务器状态的一个重要部分。有时,您需要更新数据,因为数据已经过时,或者是因为您已经有一段时间没有与页面交互了。

无论手动还是自动,React Query 都支持并允许您重新获取数据。

在本节中,我们将了解它是如何工作的,以及您可以利用哪些自动和手动方式来重新获取数据。

自动重新获取

React Query 内置了一些选项,以使您的生命更轻松并保持服务器状态新鲜。为此,它会在某些情况下自动处理数据重新获取。

让我们看看允许 React Query 自动执行数据重新获取的事物。

查询键

查询键用于标识您的查询。

在之前讨论查询键时,我多次提到我们应该将所有查询函数的依赖项作为查询键的一部分包括在内。为什么我会这么说?

因为当这些依赖项中的任何一个发生变化时,你的查询键也会发生变化,当你的查询键发生变化时,你的查询将自动重新获取。

让我们看看以下示例:

const [someVariable, setSomeVariable] = useState(0)
useQuery({
    queryKey: ["api", someVariable],
    queryFn: fetchData,
  });
return <button onClick={() => setSomeVariable
  (someVariable + 1)}> Click me </button>

在前面的代码片段中,我们定义了一个useQuery钩子,其中someVariable是其查询键的一部分。这个查询将像往常一样在初始渲染时获取,但当我们点击我们的按钮时,someVariable的值将改变。查询键也会改变,这将触发查询重新获取以获取你的新数据。

重新获取选项

在“常用选项解释”部分中,我没有分享的一些选项。这是因为它们默认启用,通常最好保留它们,除非它们不适合你的用例。

这里是useQuery默认启用的与数据重新获取相关的选项:

  • refetchOnWindowFocus:每当你的当前窗口获得焦点时,此选项会触发重新获取。例如,当你返回到你的应用程序并更改标签页时,React Query 将触发数据的重新获取。

  • refetchOnMount:每当你的钩子挂载时,此选项会触发重新获取。例如,当一个新的使用你的钩子的组件挂载时,React Query 将触发数据的重新获取。

  • refetchOnReconnect:每当你的互联网连接丢失时,此选项将触发重新获取。

有一点很重要,即默认情况下,这些选项只会重新获取你的数据,如果你的数据被标记为过时。即使数据已过时,这种数据重新获取也可以配置,因为所有这些选项(除布尔值外)也支持接收一个值为always的字符串。当这些选项的值为always时,它将始终重新触发重新获取,即使数据没有过时。

这是如何配置它们的:

useQuery({
    refetchOnMount: "always",
    refetchOnReconnect: true,
    refetchOnWindowFocus: false
  });

在前面的代码片段中,我们正在做以下操作:

  • 对于refetchOnMount选项,我们总是希望我们的钩子在任何使用它的组件挂载时重新获取我们的数据,即使缓存的数据没有过时

  • 对于refetchOnReconnect,我们希望我们的钩子在我们离线后重新获得连接时重新获取我们的数据,但只有当我们的数据已过时

  • 对于refetchOnWindowFocus,我们绝不想在窗口聚焦时让我们的钩子重新获取数据

现在,你可能想到的一个问题是是否有任何方法可以强制我们的钩子每隔几秒钟重新获取我们的数据,即使数据没有过时。好吧,即使你没有想过,React Query 也允许你这样做。

React Query 添加了另一个与重新获取相关的选项,称为refetchInterval。此选项允许你指定查询重新获取数据的频率(以毫秒为单位)。

这是如何使用它的:

useQuery({
    refetchInterval: 2000,
    refetchIntervalInBackground: true
  });

在这个片段中,我们配置我们的钩子以每 2 秒自动重新获取一次。我们还添加了一个名为refetchIntervalInBackground的选项,其值为true。此选项将允许您的查询即使在窗口或标签页处于后台时也能继续重新获取。

这总结了自动重新获取。现在,让我们看看我们如何在代码中触发手动重新获取。

手动重新获取

有两种手动触发查询重新获取的方式。您可以使用QueryClient或从钩子中获取refetch函数。

使用 QueryClient

如您可能从上一章回忆起的,QueryClient允许您在开发者和查询缓存之间建立接口。这允许您利用QueryClient在需要时强制数据重新获取。

这就是您可以使用QueryClient触发数据重新获取的方式:

const queryClient = useQueryClient();
queryClient.refetchQueries({ queryKey: ["api"] })

在前面的片段中,我们正在执行以下操作:

  • 使用useQueryClient钩子来获取对QueryClient的访问权限。

  • 使用QueryClient,我们调用它公开的一个函数,称为refetchQueries。此函数允许您触发与给定查询键匹配的所有查询的重新获取。在这个片段中,我们正在触发具有["api"]查询键的所有查询的请求。

使用重新获取函数

每个useQuery钩子都公开一个refetch函数以方便使用。此函数将允许您仅触发该查询的重新获取。

这就是您可以这样做的:

const { refetch } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
refetch()

在这个片段中,我们从useQuery钩子中解构了refetch函数。然后,我们可以在任何时候调用该函数,以强制该查询重新获取。

现在您已经知道了 React Query 如何使您能够手动和自动重新获取数据,让我们看看我们如何创建依赖于其他查询的查询。

使用useQuery获取依赖查询

有时,在开发过程中,我们需要使用一个查询返回的值,这些值可以在另一个查询中使用,或者使查询执行依赖于之前的查询。当这种情况发生时,我们需要有一个称为依赖查询的东西。

React Query 允许您通过enabled选项使一个查询依赖于其他查询。

这就是您可以这样做的:

const App = () => {
  const { data: firstQueryData } = useQuery({
    queryKey: ["api"],
    queryFn: fetchData,
  });
  const canThisDependentQueryFetch = firstQueryData?.hello
    !== undefined;
  const { data: dependentData } = useQuery({
    queryKey: ["dependentApi", firstQueryData?.hello],
    queryFn: fetchDependentData,
    enabled: canThisDependentQueryFetch,
  });
…

在前面的片段中,我们正在执行以下操作:

  1. 我们正在创建一个查询,其查询键为["api"],查询函数为fetchData函数。

  2. 接下来,我们创建一个名为canThisDependentQueryFetch的布尔变量,该变量将检查我们的上一个查询是否有我们所需的数据。这个布尔变量将帮助我们决定我们的下一个查询是否可以获取。

  3. 然后,我们创建第二个查询,其查询键为["dependentAPI", firstQueryData?.hello],查询函数为fetchDependentData函数,以及我们的canThisDependentQueryFetch作为enabled选项的Boolean变量。

当之前的查询完成数据获取后,canThisDependentQueryFetch布尔值将被设置为true,并启用此依赖查询的运行。

如您所见,您只需要enabled选项就可以使一个查询依赖于另一个查询。现在,在结束这一章之前,让我们将我们所学到的所有知识付诸实践。

将所有内容付诸实践

到目前为止,你应该能够开始处理一些使用useQuery钩子的数据获取用例。

在本节中,我们将查看一个包含三个组件的文件,这些组件分别称为ComponentAComponentBComponentC,它们正在执行一些数据获取操作。我们将使用这个文件来回顾我们已经学到的概念,并查看我们是否完全理解了useQuery的工作方式。

让我们从文件的开始部分开始:

import { useQuery, useQueryClient } from "@tanstack/react-query";
const fetchData = async ({ queryKey }) => {
  const { apiName } = queryKey[0];
  const response = await fetch(
    `https://danieljcafonso.builtwithdark.com/${apiName}`
  );
  if (!response.ok) throw new Error("Something failed in
    your request");
  return response.json();
};
const apiA = "react-query-api";
const apiB = "react-query-api-two";

在前面的代码片段中,我们正在做以下操作:

  1. 我们从 React Query 包中导入我们的useQueryuseQueryClient自定义钩子,以便在定义在接下来的几个代码片段中的组件中使用。

  2. 我们创建了一个fetchData函数,该函数将接收我们的QueryFunctionContext。然后我们从其中解构出queryKey。在这个函数内部,我们执行以下操作:

    1. 在这些示例中,我们将使用一个对象作为查询键,这样我们就可以知道数组的第一个位置将包含我们的查询键属性,因此我们从其中解构出apiName

    2. 我们使用fetch触发对 URL 的GET请求,并使用apiName来帮助定义路由。

    3. 因为我们使用的是fetch而不是axios,所以我们需要手动处理请求失败的情况。如果我们的响应不是 OK,那么我们需要抛出一个错误,以便useQuery能够处理错误场景。

    4. 如果我们的响应是有效的,那么我们可以返回我们的响应数据。

  3. 然后,我们创建了两个 API 常量值,分别称为apiAapiB,它们定义了组件将使用的路由。

现在,让我们继续我们的文件,并查看我们的第一个组件,称为ComponentA

const ComponentA = () => {
  const { data, error, isLoading, isError, isFetching } =
    useQuery({
    queryKey: [{ queryIdentifier: "api", apiName: apiA }],
    queryFn: fetchData,
    retry: 1,
  });
  if (isLoading) return <div> Loading data... </div>;
  if (isError)
    return (
      <div> Something went wrong... Here is the error:
        {error.message}</div>
    );
  return (
    <div>
      <p>{isFetching ? "Fetching Component A..." :
        data.hello} </p>
      <ComponentB/>
    </div>
  );
};

让我们回顾一下ComponentA

  1. 我们首先通过使用useQuery钩子来创建我们的查询:

    1. 这个查询使用一个对象作为查询键。这个对象有api作为queryIdentifier属性和apiA作为apiName属性。

    2. 这个查询的查询函数是fetchData函数。

    3. 通过使用retry选项,我们还指定如果这个查询在获取数据时失败,那么钩子将只重试请求一次。

    4. 我们还从钩子中解构出dataisLoadingisErrorisFetching

  2. 如果没有查询尝试完成,并且仍然没有缓存的数据,我们希望向用户显示我们正在加载数据。我们使用isLoadingIf检查来实现这一点。

  3. 如果有错误,我们希望显示它。我们使用isError来检查是否有任何错误。如果有,我们渲染那个错误。

  4. 如果我们的查询没有加载或出现错误,那么我们可以假设它是成功的。然后我们渲染一个包含以下内容的div

    • 一个p tag将检查我们的钩子isFetching。如果正在获取,它将显示Fetching Component A。如果不正在获取,它将显示获取到的数据。

    • 我们的ComponentB

现在,让我们看看ComponentB

const ComponentB = () => {
  const { data } = useQuery({
    queryKey: [{ queryIdentifier: "api", apiName: apiB }],
    queryFn: fetchData,
    onSuccess: (data) => console.log("Component B fetched
      data", data),
  });
  return (
    <div>
      <span>{data?.hello}</span>
      <ComponentC parentData={data} />
    </div>
  );
};

这就是我们ComponentB中正在做的事情:

  1. 我们首先通过使用useQuery钩子创建我们的查询:

    1. 此查询通过一个对象作为查询键进行标识。此对象具有api作为queryIdentifier属性和apiB作为apiName属性。

    2. 此查询具有fetchData函数作为查询函数。

    3. 我们使用onSuccess选项并传递一个函数,该函数将接收我们的data并在我们的console上记录它,以及指示此组件已获取数据。

    4. 我们还从钩子中解构data

  2. 然后我们返回一个div以进行渲染,如下所示:

    • 我们从获取的数据中获取的hello属性。你可能看到的一件事是我们使用了?.运算符。我们利用可选链来确保没有错误,并且只有当我们的数据定义时,我们才渲染hello属性。

    • 我们的ComponentC。此组件将接收我们的ComponentB数据作为其parentData属性。

让我们通过查看ComponentC来总结我们的文件审查:

const ComponentC = ({ parentData }) => {
  const { data, isFetching } = useQuery({
    queryKey: [{ queryIdentifier: "api", apiName: apiA }],
    queryFn: fetchData,
    enabled: parentData !== undefined,
  });
  const queryClient = useQueryClient();
  return (
    <div>
      <p>{isFetching ? "Fetching Component C..." :
        data.hello} </p>
      <button
        onClick={() =>
          queryClient.refetchQueries({
            queryKey: [{ queryIdentifier: "api",
              apiName: apiA }],
          })
        }
      >
        Refetch Parent Data
      </button>
    </div>
  );
};
export default ComponentA;

因此,这是ComponentC中正在发生的事情:

  1. 我们首先通过使用useQuery钩子创建我们的查询:

    1. 此查询通过一个对象作为查询键进行标识。此对象具有api作为queryIdentifier属性和apiA作为apiName属性。

    2. 此查询具有fetchData函数作为查询函数。

    3. 我们使用enabled选项使此查询依赖于parentData;因此,此查询只有在ComponentB中的查询完成并解析数据后才会运行。

    4. 我们从钩子中解构dataisFetching

  2. 我们使用useQueryClient钩子来获取对QueryClient的访问权限。

  3. 最后,我们返回一个将渲染以下内容的div

    • 一个p标签,将检查我们的钩子isFetching。如果正在获取,则显示Fetching Component C。如果不,则显示获取的数据。

    • 一个按钮,当点击时,将使用queryClient重新获取查询键具有api作为queryIdentifier属性和apiA作为apiName属性的查询。这意味着在此按钮点击时,ComponentAComponentC中的useQuery都将重新获取一些数据。

此外,在前面的代码片段中,我们默认导出我们的ComponentA,因此它是此文件的入口点。

现在我们已经看到了代码文件,让我们回顾钩子的生命周期并了解后台发生了什么:

  • ComponentA渲染时,以下情况会发生:

    • 一个具有[{ queryIdentifier: "api", apiName: apiA }]查询键的useQuery实例挂载:

      • 由于这是第一次挂载,没有缓存也没有之前的请求,因此我们的查询将开始获取我们的数据,其status将为加载状态。此外,我们的查询函数将作为QueryFunctionContext的一部分接收我们的查询键。

      • 当我们的数据获取成功时,数据将根据[{ queryIdentifier: "api", apiName: apiA }]查询键进行缓存。

      • 由于我们假设默认的staleTime,其值为0,钩子将标记其数据为过时。

  • ComponentA渲染ComponentB时,以下情况会发生:

    • 具有查询键[{ queryIdentifier: "api", apiName: apiB }]useQuery实例挂载:

      • 由于这是第一次挂载,没有缓存也没有之前的请求,因此我们的查询将开始获取数据,其状态将是加载中。此外,我们的查询函数将作为QueryFunctionContext的一部分接收我们的查询键。

      • 当我们的数据获取成功时,数据将根据[{ queryIdentifier: "api", apiName: apiB }]查询键进行缓存,并且钩子将调用onSuccess函数。

      • 由于我们假设默认的staleTime,即0,钩子将标记其数据为过时。

  • ComponentB渲染ComponentC时,以下情况发生:

    • 具有查询键[{ queryIdentifier: "api", apiName: apiA }]useQuery实例挂载:

      • 由于此钩子与ComponentA中的钩子具有相同的查询键,钩子下已经缓存了数据,因此数据可以立即访问。

      • 由于此查询在之前的获取后标记为过时,此钩子需要重新获取它,但它需要等待查询首先启用,因为此查询依赖于我们首先拥有ComponentB的数据。

      • 一旦启用,查询将触发重新获取。这使得ComponentAComponentC上的isFetching都变为true

      • 一旦获取请求成功,数据将根据[{ queryIdentifier: "api", apiName: apiA }]查询键进行缓存,并且查询再次标记为过时。

  • 现在,考虑到它是父组件,让我们设想一个场景,其中ComponentA卸载:

    • 由于不再有任何具有[{ queryIdentifier: "api", apiName: apiA }]查询键的活动查询实例,默认的缓存超时设置为 5 分钟。

    • 一旦过去 5 分钟,此查询下的数据将被删除并回收。

    • 由于不再有任何具有[{ queryIdentifier: "api", apiName: apiB }]查询键的活动查询实例,默认的缓存超时设置为 5 分钟。

    • 一旦过去 5 分钟,此查询下的数据将被删除并回收。

如果你能够跟踪此前的过程以及查询在它们使用过程中的生命周期,那么恭喜你:你理解了useQuery是如何工作的!

摘要

在本章中,我们学习了useQuery自定义钩子以及它是如何通过使用其必需的选项(称为查询键和查询函数)来获取和缓存数据的。你学习了如何定义查询键以及你的查询函数如何允许你使用任何数据获取客户端,如 GraphQL 或 REST,只要它返回一个承诺或抛出一个错误。

你还了解了一些useQuery钩子返回的内容,例如查询的dataerror。为了构建更好的用户体验,你还介绍了statusfetchStatus

为了让你自定义开发者体验并将其提升到下一个层次,你了解了一些常用的选项,你可以使用这些选项来自定义你的useQuery钩子,使其按你的意愿运行。为了你的方便,以下是一些需要注意的编译默认值:

  • staleTime: 0

  • cacheTime: 5 * 60 * 1,000 (5 minutes)

  • retry: 3

  • retryDelay: 指数退避延迟算法

  • enabled: True

在结束之前,你了解了一些处理服务器状态挑战(如重新获取和依赖查询)的模式。

最后,你将所学的一切付诸实践,并回顾了一个示例,展示了如何利用所有这些知识,以及当你这样做时useQuery钩子是如何在内部工作的。

第五章《更多数据获取挑战》中,你将继续学习如何使用useQuery钩子来解决一些更常见的服务器状态挑战,例如数据预取、分页请求和无限制查询。你还将使用开发者工具来帮助你调试查询。

第五章:更多数据获取挑战

到现在为止,你必须熟悉 React Query 如何通过 useQuery 帮助你获取数据。你甚至学习了如何处理服务器状态带来的某些常见挑战。

在本章中,你将学习如何处理一些更多的服务器状态挑战。你将了解你如何执行并行查询,在这个过程中,你将了解一个使 useQuery 钩子更容易使用的变体,称为 useQueries

你将再次利用 QueryClient 来处理数据预取、查询无效化和查询取消。你甚至将学习如何通过使用一些过滤器来自定义你用来做这些事情的方法。

useQuery 以及甚至另一个名为 useInfiniteQuery 的变体。

到本章结束时,你将再次使用 Devtools 来查看你的查询,并增强对其的调试。

在本章中,我们将涵盖以下主题:

  • 构建并行查询

  • 利用 QueryClient

  • 创建分页查询

  • 创建无限查询

  • 使用 Devtools 调试你的查询

技术要求

本章的所有代码示例都可以在 GitHub 上找到,链接为 github.com/PacktPublishing/State-management-with-React-Query/tree/feat/chapter_5

构建并行查询

我们经常发现需要使用的一个典型模式是并行查询。并行查询是指同时执行的查询,以避免有顺序的网络请求,通常称为网络瀑布。

并行查询可以帮助你通过同时发送所有请求来避免网络瀑布。

React Query 允许我们以两种方式执行并行查询:

  • 手动

  • 动态地

手动并行查询

如果我现在要求你正确地执行并行查询,这可能是你可能会这样做的方式。它只涉及并排编写任意数量的 useQuery 钩子。

当你需要执行固定数量的并行查询时,这种模式非常出色。这意味着你将执行查询的数量始终相同,不会改变。

这就是你可以按照这种方法编写并行查询的方式:

const ExampleOne = () => {
  const { data: queryOneData  } = useQuery({
    queryKey: [{ queryIdentifier: "api", username:
      "userOne" }],
    queryFn: fetchData,
  });
  const { data: queryTwoData } = useQuery({
    queryKey: [{ queryIdentifier: "api", username:
      "userTwo" }],
    queryFn: fetchData,
  });
  const { data: queryThreeData } = useQuery({
    queryKey: [{ queryIdentifier: "api", username:
      "userThree" }],
    queryFn: fetchData,
  });
  return (
    <div>
      <p>{queryOneData?.hello}</p>
      <p>{queryTwoData?.hello}</p>
      <p>{queryThreeData?.hello}</p>
    </div>
  );
};

在前面的代码片段中,我们通过向所有这些查询添加不同的查询键来创建三个不同的查询。这些查询将并行获取,一旦查询函数解决,我们将能够访问它们的数据。然后我们使用这些数据在 p 标签内渲染它们的 hello 属性。

动态并行查询

虽然手动并行查询适用于大多数场景,但如果你的查询数量变化,你将无法在不违反钩子规则的情况下使用它。为了处理这个问题,React Query 创建了一个名为 useQueries 的自定义钩子。

useQueries 允许你动态地调用你想要的任意数量的查询。以下是它的语法:

const queryResults = useQueries({
  queries: [
    { queryKey: ["api", "queryOne"], queryFn: fetchData },
    { queryKey: ["api", "queryTwo"], queryFn: fetchData }
  ]
})

如前所述的代码片段所示,useQueries钩子在它的queries属性中接收一个查询数组。这些查询甚至可以接收选项,所以你应该有这样的心理模型:这些查询可以像useQuery钩子一样进行定制。

useQueries钩子将返回一个包含所有查询结果的数组。

既然你已经了解了useQueries的工作原理,让我们在下面的代码片段中将其付诸实践:

const usernameList = ["userOne", "userTwo", "userThree"];
const ExampleTwo = () => {
  const multipleQueries = useQueries({
    queries: usernameList.map((username) => {
      return {
        queryKey: [{ queryIdentifier: "api", username }],
        queryFn: fetchData,
      };
    }),
  });
  return (
    <div>
      {multipleQueries.map(({ data, isFetching }) => (
        <p>{isFetching ? "Fetching data..." : data.hello}
          </p>
      ))}
    </div>
  );
};

在前面的代码片段中,我们做了以下操作:

  1. 我们创建一个usernameList字符串数组来帮助我们创建一些动态查询。

  2. 在我们的useQueries钩子内部,对于usernameList中的每个实例,我们创建一个相应的查询,包括其查询键和查询函数。

  3. 我们使用useQueries钩子的结果;对于它内部的每个项目,我们利用isFetching向用户显示我们正在获取数据。如果它没有获取数据,那么我们假设我们已经完成了我们的请求,并显示获取到的数据。

现在你已经知道如何利用useQueryuseQueries来执行并行查询,让我们看看你如何利用QueryClient来解决一些更多的服务器状态挑战。

利用 QueryClient

正如你所知,QueryClient允许你与你的缓存进行交互。

在上一章中,我们看到了如何利用QueryClient来触发查询的重新获取。我们还没有看到的是QueryClient可以用于更多的事情。

要在你的组件中使用QueryClient,你可以利用useQueryClient钩子来访问它。然后,你所要做的就是调用你需要的那个方法。

在本节中,我们将看到如何使用QueryClient来解决更多服务器状态挑战,例如以下内容:

  • 查询无效化

  • 预取

  • 查询取消

在我们开始查询无效化之前,有一件事需要注意,那就是其中一些方法,即我们即将看到的方法,可以接收某些查询过滤器来帮助你匹配正确的查询。

在上一章中,我们看到了查询重新获取的以下示例:

queryClient.refetchQueries({ queryKey: ["api"] })

前面的代码片段是一个示例,说明我们可以在refetchQueries方法中提供一个过滤器。在这种情况下,我们正在尝试重新获取所有匹配或以查询键["api"]开头的查询。

现在,你可以使用除了查询键之外的更多过滤器。在QueryClient方法中使用的过滤器,通常称为QueryFilters,支持以下类型的过滤:

  • 查询键

  • 查询类型

  • 查询是否过时或新鲜

  • fetchStatus

  • 一个谓词函数

这里有一些使用QueryFilters的示例。

在下面的示例中,我们使用type过滤器与active值一起重新获取所有当前处于活动状态的查询:

queryClient.refetchQueries({ type: "active" })

在下面的示例中,我们使用stale过滤器与true值一起重新获取所有staleTime已过期的查询,现在被认为是过时的:

queryClient.refetchQueries({ stale: true })

在以下示例中,我们使用fetchStatus过滤器与idle值一起重新获取所有当前未获取任何内容的查询:

queryClient.refetchQueries({ fetchStatus: "idle"})

在以下示例中,我们使用predicate属性并向其传递一个匿名函数。此函数将接收正在验证的查询并访问其当前状态;如果此状态是错误,则函数将返回true。这意味着所有当前状态为错误的查询都将重新获取。

queryClient.refetchQueries({
            predicate: (query) => query.state.status ===
              "error",
})

现在,您不需要只传递一个过滤器。您可以发送以下组合的过滤器:

queryClient.refetchQueries({ queryKey: ["api"], stale: true
  })

在前面的示例中,我们重新获取了所有以["api"]开头的过时查询。

如果您不想传递任何过滤器并希望方法应用于所有查询,您可以选择不传递任何过滤器,如下所示:

queryClient.refetchQueries()

此示例将重新获取所有查询。

您现在熟悉了QueryFilters,并可以看到其中涉及的一些服务器状态挑战。让我们从查询无效化开始。

查询无效化

有时,独立于您配置的staleTime,您的数据可能会变得过时。为什么,您可能会问?好吧,有时可能是因为您执行的突变;有时可能是因为其他用户在某个地方与您的服务器状态进行了交互。

当这种情况发生时,您可以使用您的QueryClient invalidateQueries方法将查询标记为过时。

这里是invalidateQueries方法的语法:

queryClient.invalidateQueries({ queryKey: ["api"] })

通过调用invalidateQueries,所有匹配或以["api"]开头的查询都将被标记为stale,如果已配置,则覆盖其staleTime。如果您的查询是活动状态,因为useQuery钩子渲染正在使用它,那么 React Query 将负责重新获取该查询。

让我们现在通过以下示例将其付诸实践:

const QueryInvalidation = () => {
  const { data } = useQuery({
    queryKey: [{ queryIdentifier: "api", username:
      "userOne" }],
    queryFn: fetchData,
  });
  const queryClient = useQueryClient();
  return (
    <div>
      <p>{data?.hello}</p>
      <button
        onClick={() =>
          queryClient.invalidateQueries({
            queryKey: [{ queryIdentifier: "api" }],
          })
        }
      >
        Invalidate Query
      </button>
    </div>
  );
};

在前面的代码片段中,我们有一个无效化查询的示例。这就是我们正在做的事情:

  1. 创建一个由[{ queryIdentifier: "api", username: "userOne" }]查询键标识的查询

  2. 获取queryClient访问权限

  3. 渲染我们的查询数据和按钮,其中onClick将使所有匹配或包含查询键的一部分[{ queryIdentifier: "api" }]的查询无效

当用户点击查询键的一部分[{ queryIdentifier: "api" }]时,该查询数据将立即被标记为stale。由于此查询正在渲染中,它将自动在后台重新获取。

预取

您希望用户获得最佳的用户体验。这有时意味着在用户意识到之前就了解他们的需求。这正是预取可以帮助您的地方。

当您可以预测用户可能想要执行的操作,这不可避免地触发查询时,您可以利用这些知识并预取查询以节省用户未来的时间。

QueryClient允许您访问一个名为prefetchQuery的方法来预取您的数据。

这里是prefetchQuery方法的语法:

queryClient.prefetchQuery({
      queryKey: ["api"],
      queryFn: fetchData
  });

prefetchQuery需要一个查询键和一个查询函数。这个方法将尝试获取你的数据并将其缓存到给定的查询键下。这是一个异步方法;因此,你需要等待它完成。

现在我们来看一个使用我们的ExamplePrefetching组件进行数据预取的实际例子:

const ExamplePrefetching = () => {
  const [renderComponent, setRenderComponent] =
    useState(false);
  const queryClient = useQueryClient();
  const prefetchData = async () => {
    await queryClient.prefetchQuery({
      queryKey: [{ queryIdentifier: "api", username:
        "userOne" }],
      queryFn: fetchData,
      staleTime: 60000
    });
  };
  return (
    <div>
      <button onMouseEnter={prefetchData} onClick={() =>
      setRenderComponent(true)}> Render Component </button>
      {renderComponent ? <PrefetchedDataComponent /> : null
        }
    </div>
  );
};

在前面的代码片段中,我们创建了我们的ExamplePrefetching组件。以下是它的作用:

  1. 它创建了一个状态变量,我们将使用它来允许我们渲染PrefetchedDataComponent

  2. 它可以访问queryClient

  3. 它创建了一个名为prefetchData的函数,我们在其中调用prefetchQuery方法并将返回的数据缓存到[{ queryIdentifier: "api", username: "userOne" }]查询键下。我们还给它一个staleTime为 1 分钟,所以调用这个查询后,数据将被认为在 1 分钟内是新鲜的。

  4. 创建一个按钮,当点击时,将改变我们的状态变量以允许我们渲染PrefetchedDataComponent。此按钮还有一个onMouseEnter事件,它将触发我们的数据预取。

现在我们来看一下我们的PrefetchedDataComponent组件:

const PrefetchedDataComponent = () => {
  const { data } = useQuery({
    queryKey: [{ queryIdentifier: "api", username:
      "userOne" }],
    queryFn: fetchData,
  });
  return <div>{data?.hello}</div>;
};

在前面的代码片段中,我们可以看到PrefetchedDataComponent。这个组件有一个由[{ queryIdentifier: "api", username: "userOne" }]查询键标识的查询。当这些数据存在时,它将被渲染在div内部。

因此,让我们回顾一下这两个组件的用户流程:

  1. ExamplePrefetching被渲染。

  2. 用户将看到一个写着渲染组件的按钮。

  3. 用户将鼠标放在按钮上准备点击。此时,我们预测用户将点击按钮,因此我们触发数据预取。一旦数据被预取,它就会被缓存到[{ queryIdentifier: "api", username: "userOne" }]查询键下。

  4. 用户点击按钮。

  5. PrefetchedDataComponent被渲染。

  6. [{ queryIdentifier: "api", username: "userOne" }]查询键标识的useQuery钩子已经将数据缓存并标记为在一分钟内是新鲜的,因此不需要触发数据获取。

  7. 用户看到预取的数据被渲染。

查询取消

有时候,当你的useQuery钩子在查询过程中卸载时,你的查询可能会被卸载。默认情况下,一旦你的承诺被解决,这个查询数据仍然会被接收并缓存。但是,出于某种原因,你可能希望在数据获取请求进行到一半时取消你的查询。React Query 可以通过自动取消你的查询来处理这个问题。你也可以手动取消你的查询。

为了允许你取消你的查询,React Query 使用一个可以与 DOM 请求通信并中止它们的信号。这个信号是AbortSignal对象,它属于AbortController Web API。

AbortSignal信号通过QueryFunctionContext注入到我们的查询函数中,然后它应该被我们的数据获取客户端消耗。

这是我们可以如何利用 AbortSignalaxios

const fetchData = async ({ queryKey, signal }) => {
  const { username } = queryKey[0];
  const { data } = await axios.get(
    `https://danieljcafonso.builtwithdark.com/
      react-query-api/${username}`,
    { signal }
  );
  return data;
};

在前面的段中,我们从 QueryFunctionContext 接收 signal 并将其作为选项在我们的 axios 客户端进行 get 请求时传递。

如果你在一个使用 GraphQL 的场景中使用 axios 的替代品,例如 fetchgraphql-request,你也需要将 AbortSignal 传递给你的客户端。

这就是你可以使用 fetch 来做到这一点的方式:

const fetchDataWithFetch = async ({ queryKey, signal }) => {
  const { username } = queryKey[0];
  const response = await fetch(
    `https://danieljcafonso.builtwithdark.com/
      react-query-api/${username}`,
    { signal }
  );
  if (!response.ok) throw new Error("Something failed in
    your request");
  return response.json();
};

在前面的段中,我们从 QueryFunctionContext 接收 signal 并将其作为选项传递给我们的 fetch 调用。

如果你使用 graphql-request 这样的 GraphQL 客户端,这是你可以做到这一点的方式:

const fetchGQL = async ({signal}) => {
  const endpoint = <Add_Endpoint_here>;
  const client = new GraphQLClient(endpoint)
  const {
    posts: { data },
  } = await client.request({document: customQuery,
    signal});
  return data;
};

在前面的段中,我们也从 QueryFunctionContext 接收 signal 并将其作为选项传递给我们的客户端请求。

将信号传递给我们的客户端只是允许他们取消查询的第一步。你需要触发自动查询取消或手动取消。

手动取消

对于手动取消查询,QueryClient 提供了访问 **cancelQueries** 方法的权限。

这是 cancelQueries 方法的语法:

queryClient.cancelQueries({ queryKey: ["api"] })

通过调用 cancelQueries,所有匹配或以 ["api"] 开头的当前正在获取且已接收 AbortSignal 的查询都将被中止。

自动取消

当使用你的钩子的组件卸载且你的查询正在获取数据时,如果你向客户端传递 AbortSignal,React Query 将通过取消承诺来中止你的查询。

让我们看看 React Query 如何通过以下示例利用 AbortSignal 来取消你的查询。首先,我们开始配置我们的查询函数:

const fetchData = async ({ queryKey, signal }) => {
  const { username } = queryKey[0];
  const { data } = await axios.get(
    `https://danieljcafonso.builtwithdark.com/
      react-query-api/${username}`,
    { signal }
  );
  return data;
};

在前面的段中,我们创建了一个 fetchData 函数,该函数将接收 QueryContextObject。从中,我们获取对 signal 的访问权限并将其传递给我们的 axios 客户端。

现在,让我们看看我们的组件:

const ExampleQueryCancelation = () => {
  const [renderComponent, setRenderComponent] =
    useState(false);
  return (
    <div>
      <button onClick={() => setRenderComponent
        (!renderComponent)}>
        Render Component
      </button>
      {renderComponent ? <QueryCancelation /> : null}
    </div>
  );
};

在前面的段中,我们有一个名为 ExampleQueryCancelation 的组件。这个组件将在用户点击按钮的任何地方渲染和卸载一个名为 QueryCancelation 的组件。

让我们现在看看 QueryCancelation 组件:

const QueryCancelation = () => {
  const { data } = useQuery({
    queryKey: [{ queryIdentifier: "api", username:
      "userOne" }],
    queryFn: fetchData,
  });
  const queryClient = useQueryClient();
  return (
    <div>
      <p>{data?.hello}</p>
      <button
        onClick={() =>
          queryClient.cancelQueries({
            queryKey: [{ queryIdentifier: "api" }],
          })
        }
      >
        Cancel Query
      </button>
    </div>
  );
};

段落显示了 QueryCancelation 组件。在这个组件中,我们做以下操作:

  1. 我们创建了一个由 [{ queryIdentifier: "api", username: "userOne" }] 查询键标识的查询。

  2. 我们获取对 QueryClient 的访问权限。

  3. 我们从查询中渲染我们的 data

  4. 我们渲染一个按钮,当点击时,将使用 QueryClient 取消所有匹配或包含其键中的 [{"queryIdentifier": "api"}] 的查询。

让我们现在回顾这些组件的生命周期以及查询取消如何运作:

  1. 我们渲染 ExampleQueryCancelation 组件。

  2. 我们点击按钮以渲染 QueryCancelation 组件。

  3. QueryCancelation 被渲染,其 useQuery 钩子将触发一个请求以获取其数据。

  4. 在这个请求期间,我们再次点击按钮以渲染 QueryCancelation

  5. 由于我们的请求尚未解决且我们的组件已卸载,React Query 将中止我们的信号,这将取消我们的请求。

  6. 我们点击按钮再次渲染QueryCancelation组件。

  7. QueryCancelation被渲染,其useQuery钩子将触发一个请求来获取其数据。

  8. 在这次请求过程中,我们点击按钮取消我们的查询。这将强制 React Query 终止我们的信号并再次取消我们的请求。

因此,我们已经看到了QueryClient及其一些方法如何帮助我们解决一些常见的服务器状态挑战。

在下一节中,我们将看到 React Query 如何允许我们构建一个常见的 UI 模式,即分页查询。

创建分页查询

当构建一个处理大量数据的 API 时,为了避免你的前端一次性处理所有内容,你不想在一个请求中发送所有可用的数据。一种常用的模式是 API 分页。

如果你的 API 是分页的,你希望将相同的模式应用到你的应用程序中。

好处在于,你只需要使用useQuery及其一个选项,keepPreviousData

让我们看看接下来的示例,然后了解分页和 React Query 是如何工作的。首先,我们从我们的查询函数开始:

const fetchData = async ({ queryKey }) => {
  const { page } = queryKey[0];
  const { data } = await axios.get(
    `https://danieljcafonso.builtwithdark.com/
      react-query-paginated?page=${page}&results=10`
  );
  return data;
};

在前面的代码片段中,我们创建了一个将用作查询函数的函数。由于这是一个分页 API,我们需要page来获取我们的数据。正如我们在上一章中建立的,如果变量是查询的依赖项,则需要将其添加到查询键中。然后,我们在查询函数中从查询键中解构page。然后,我们只需要获取我们的数据,并在承诺解决时返回它。

现在,让我们看看我们如何构建一个用于显示和获取分页数据的组件:

const PaginatedQuery = () => {
  const [page, setPage] = useState(0);
  const { isLoading, isError, error, data, isFetching,
    isPreviousData } =
    useQuery({
      queryKey: [{ queryIdentifier: "api", page }],
      queryFn: fetchData,
      keepPreviousData: true,
    });
  if (isLoading) {
    return <h2>Loading initial data...</h2>;
  }
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  return (
    <>
      <div>
        {data.results.map((user) => (
          <div key={user.email}>
            {user.name.first}
            {user.name.last}
          </div>
        ))}
      </div>
      <div>
      <button
        onClick={() => setPage((oldValue) => oldValue === 0
          ? 0 : oldValue - 1)}
        disabled={page === 0}
      >
        Previous Page
      </button>
      <button
        disabled={isPreviousData}
        onClick={() => {
          if (!isPreviousData) setPage((old) => old + 1);
        }}
      >
        Next Page
      </button>
      </div>
      {isFetching ? <span> Loading...</span> : null}
    </>
  );
};

让我们回顾一下前面代码块中发生的事情:

  1. 我们创建一个状态变量来保存我们当前选中的page

  2. 我们创建我们的查询,它具有[{ queryIdentifier: "api", page }]查询键,我们的fetchData函数作为查询函数,并将keepPreviousData设置为true。我们将此选项设置为true是因为,默认情况下,每当我们的查询键更改时,查询数据也会更改;现在,由于我们有一个分页 API,我们希望即使在更改页面时也能继续显示我们的数据。

  3. 然后,我们解构isLoadingisErrorerrordataisFetchingisPreviousDataisPreviousData用于指示当前显示的数据是否是上一个版本。

  4. 我们有两个if语句来显示我们的查询何时正在加载,或者何时出现错误。

  5. 如果我们有数据,我们显示它,并有两个按钮用于移动到下一页和上一页。用于移动到下一页的按钮利用isPreviousData确保我们在点击并移动到后续查询后将其禁用。我们还显示一个获取指示器。

现在我们已经看到了代码的结构,让我们看看当与之交互时的行为:

  1. 我们的组件被渲染,第一页开始被获取。

isLoading属性被设置为true,因此我们渲染Loadinginitial data

  1. 第一页的数据已解析,因此我们显示它。

  2. 我们点击 page 值会增加。

  3. 查询键发生变化,因此接下来的查询开始获取。

  4. 由于我们将 keepPreviousData 设置为 true,我们仍然会显示旧数据。

  5. 由于我们正在显示旧数据,isPreviousData 被设置为 true,并且显示 Loading

  • 我们获取新数据并显示它。* 我们点击 Loading。* 新数据被接收并显示。

如您所见,您只需要一个新的选项和相同的旧 useQuery 钩子,就可以构建一个使用分页的应用程序。

在下一节中,让我们看看如何构建无限查询。

创建无限查询

另一个非常常见的 UI 模式是构建无限滚动组件。在这个模式中,我们看到一个列表,允许我们在向下滚动时加载更多数据。

为了处理这些类型的列表,React Query 提供了 useQuery 钩子的一个替代品,这是一个名为 useInfiniteQuery 的自定义钩子。

使用 useInfiniteQuery 钩子与 useQuery 钩子有很多相似之处,但也有一些不同之处,我们需要注意:

  • 您的数据现在是一个包含以下内容的对象:

    • 获取的页面

    • 用于获取页面的 page 参数

  • 一个名为 fetchNextPage 的函数,用来获取下一页

  • 一个名为 fetchPreviousPage 的函数,用来获取上一页

  • 一个名为 isFetchingNextPage 的布尔状态,用来指示下一页正在被获取

  • 一个名为 isFetchingPreviousPage 的布尔状态,用来指示下一页正在被获取

  • 一个名为 hasNextPage 的布尔状态,用来指示列表是否有下一页

  • 一个名为 hasPreviousPage 的布尔状态,用来指示列表是否有上一页

这最后两个布尔值取决于可以传递给钩子的两个选项。分别是 getNextPageParamgetPreviousPageParam。这些函数将负责选择缓存中的最后一页或第一页,并检查其数据是否指示要获取下一页或上一页。如果这些值存在,则相应的布尔值将为 true。如果它们返回 undefined,则布尔值将为 false

要使用 useInfiniteQuery 钩子,您需要以这种方式导入它:

import { useInfiniteQuery } from "@tanstack/react-query"

现在让我们看看如何使用 useInfiniteQuery 钩子构建一个无限列表的示例:

const fetchData = async ({ pageParam = 1 }) => {
    const { data } = await axios.get(
        `https://danieljcafonso.builtwithdark.com/
         react-query-infinite?page=${pageParam}&results=10`
    );
    return data;
  };

在前面的代码片段中,我们设置了用作无限查询函数的函数。钩子将传递 pageParamQueryFunctionContext,这样我们就可以利用它来获取我们的数据。像 useQuery 钩子中的查询函数一样,这个查询函数需要解决数据或抛出错误,因此所有之前学到的原则都适用。

下一个代码片段将展示我们的 InfiniteScroll 组件:

const InfiniteScroll = () => {
  const {
    isLoading,
    isError,
    error,
    data,
    fetchNextPage,
    isFetchingNextPage,
    hasNextPage,
  } = useInfiniteQuery({
    queryKey: ["api"],
    queryFn: fetchData,
    getNextPageParam: (lastPage, pages) => {
      return lastPage.info.nextPage;
    },
  });
  if (isLoading) {
    return <h2>Loading initial data...</h2>;
  }
  if (isError) {
    return <h2>{error.message}</h2>;
  }
  return (
    <>
      <div>
        {data.pages.map((page) =>
          page.results.map((user) => (
            <div key={user.email}>
              {user.name.first}
              {user.name.last}
            </div>
          ))
        )}
      </div>
      <button
        disabled={!hasNextPage || isFetchingNextPage}
        onClick={fetchNextPage}
      >
        {isFetchingNextPage
          ? «Loading...»
          : hasNextPage
          ? «Load More»
          : «You have no more data»}
      </button>
    </>
  );
};

在前面的代码片段中,我们有一个渲染无限列表的组件。这就是我们在组件中做的事情:

  1. 我们创建useInfiniteQuery,它以["api"]作为查询键和fetchData作为查询函数。它还接收一个匿名函数在getNextPageParam选项中,以检查下一页是否还有更多数据要加载。

  2. 我们还从钩子中解构出构建我们的应用程序所需的某些变量。

  3. 然后我们有两个if语句来显示我们的查询正在加载或存在错误时的情况。

  4. 当我们有数据时,我们将其page属性内的内容映射以渲染我们的列表。

  5. 我们还渲染了一个按钮,如果我们没有下一页或我们正在获取下一页时,该按钮将被禁用。当点击时,此按钮将获取更多数据。此按钮消息也将取决于一些约束:

    • 如果我们在获取数据,下一页将显示一个加载消息

    • 如果我们有下一页,它将显示加载更多,以便用户可以点击它开始获取

    • 如果没有更多数据可以获取,它将显示一条消息,告知用户没有更多数据

正如我们刚刚回顾了组件的构建方式,让我们看看它与交互时的表现:

  1. 我们的组件渲染,列表的第一页将自动获取:

    • isLoading属性设置为true,所以我们渲染加载 初始数据
  2. 列表的第一页数据已解析,所以我们显示它。

  3. 同时,getNextPageParam函数检查列表中是否有更多数据。

  4. 如果没有更多数据,hasNextPage属性设置为false,获取更多数据的按钮被禁用并显示您没有 更多数据

  5. 如果有更多数据,hasNextPage属性设置为true,用户可以点击按钮来获取更多数据。

  6. 如果用户点击按钮,我们将看到以下内容:

    1. 下一页开始获取。

    2. isFetchingNextPage的值变为true

    3. 按钮被禁用并显示加载消息。

    4. 数据已解析,并且我们的数据pages属性长度增加,因为它包含了新页面的数据。步骤 3、4、5被重复。

通过这种方式,我们刚刚看到了useQuery变体useInfiniteQuery如何让我们直接构建无限列表。

在我们结束这一章之前,让我们最后看看我们如何使用 React Query Devtools 来帮助我们调试代码并查看我们的查询行为。

使用 Devtools 调试查询

第三章中,你学习了关于 React Query Devtools 的内容。在那个阶段,你还不知道如何使用查询,所以我们无法看到它的工作情况。现在我们可以了。

对于你接下来要看到的图像,我们将利用我们在动态并行 查询部分向你展示useQueries钩子示例时编写的代码。

为了让您记住,这里是有代码:

const usernameList = ["userOne", "userTwo", "userThree"];
const ExampleTwo = () => {
  const multipleQueries = useQueries({
    queries: usernameList.map((username) => {
      return {
        queryKey: [{ queryIdentifier: "api", username }],
        queryFn: fetchData,
      };
    }),
  });
  return (
    <div>
      {multipleQueries.map(({ data, isFetching }) => (
        <p>{isFetching ? "Fetching data..." : data.hello}
          </p>
      ))}
    </div>
  );
};

当使用该代码并检查我们的页面时,Devtools 将向我们展示以下内容:

图 5.1 – 执行并行查询后的 React Query Devtools

图 5.1 – 执行并行查询后的 React Query Devtools

在前面的图中,我们可以看到以下内容:

  • 我们有三个查询

  • 每个查询都通过各自的查询键来标识

  • 所有查询目前都是过时的

  • 我们已选择了以[{ queryIdentifier: "api", username: "userThree" }]查询键标识的查询

当我们选择一个查询时,我们可以在我们的查询详情标签页中看到查询详情。

在前面的图中,我们可以看到这个查询通过其查询键和状态来标识。

查询详情标签页向下滚动,我们还能看到以下内容:

图 5.2 – React Query Devtools 查询详情标签页显示操作和数据探索器

图 5.2 – React Query Devtools 查询详情标签页显示操作和数据探索器

在前面的图中,我们可以看到我们可以为选定的查询执行几个操作,例如重新获取、使无效、重置和删除它。

我们也能看到这个查询的当前数据。

在我们的查询详情标签页进一步向下滚动,我们还可以检查查询探索器

图 5.3 – React Query Devtools 查询详情标签页显示查询探索器

图 5.3 – React Query Devtools 查询详情标签页显示查询探索器

在前面的图中,我们可以看到cacheTime300000

您现在已经了解在 Devtools 中为每个选定的查询可以看到什么。

在结束本节之前,让我们看看点击查询详情操作中可用的按钮之一会发生什么:

图 5.4 – React Query Devtools 当前正在获取查询

图 5.4 – React Query Devtools 当前正在获取查询

在前面的图中,我们点击了[{ queryIdentifier: "api", username: "userTwo" }]查询键。

如您在学习查询无效化时记得的那样,当我们使查询无效时,它会自动标记为过时,如果查询当前正在渲染,它将自动重新获取。从图中可以看出,这就是发生的情况。我们的查询已经过时,因此没有必要再次将其标记为过时,但由于它目前正在我们的页面上渲染,React Query 负责重新获取它,我们可以在图中看到这一点。

如您在本节中看到的那样,Devtools 可以为您节省大量调试查询的时间。通过查看您的查询,如果您已配置了正确的选项,您可以检查其数据看起来如何,甚至可以触发一些操作。

摘要

在本章中,我们学习了更多关于使用useQuery钩子来解决我们在处理服务器状态时遇到的一些常见挑战。到现在为止,您可以轻松处理所有数据获取需求。

你学习了并行查询,并了解到你可以使用useQuery手动构建这些查询。你还被介绍到useQuery钩子的一种替代方案:useQueries。通过它,你学习了如何构建动态并行查询。

你需要更多地了解一些QueryClient的方法,这些方法允许你预取、取消和使查询无效,并且你也理解了如何利用QueryFilters来自定义这些方法中使用的查询匹配。

分页是一个典型的 UI 模式,现在你知道你可以借助useQuery及其一个选项轻松构建分页组件。

另一个典型的 UI 模式是无限滚动。借助另一个名为useInfiniteQueryuseQuery变体,你学习了 React Query 如何让你构建一个具有无限列表的应用程序。

最后,你使用 React Query Devtools 检查了你的查询,并理解了它如何允许你调试它们并改进你的开发过程。

第六章使用 React Query 执行数据突变中,我们将放下数据获取,转向突变。你将理解 React Query 如何通过其名为useMutation的自定义钩子帮助你执行突变。你还将利用这个钩子来处理你在应用程序中遇到的更常见的服务器状态挑战,并通过使用乐观更新来开始构建更好的用户体验。