使用 React Query 解决 React 应用中的状态管理

2025-06-09

使用 React Query 解决 React 应用中的状态管理

我最近有机会(也是荣幸!)在目前的公司启动一个全新项目,涉及一个内部工具的前端应用程序。参与的开发人员有机会选择我们认为方便的技术栈,我们合作撰写了一份 RFC(征求意见稿),并将其提交给公司其他成员,以开放我们的选择进行讨论。

在选择了公司通用框架 React 之后,我们讨论的重点之一就是如何处理状态管理。我们的主要应用使用了 Redux,但也提出了许多其他替代方案:MobX、使用原生 Hooks(useReducer + useContext 的组合)、使用 Redux 和 Redux Toolkit。我甚至了解并提出了 Recoil,这是一个非常令人兴奋的项目,而且绝对是我迄今为止看过的最好的演示视频之一。

但我们的工程师 Zac 提出了一个不同的想法。于是就有了 React-Query。

React Query 的状态管理新方法

“我还没用过它,但我很喜欢它处理应用程序内部状态的独特方法。它基本上将服务器端状态与客户端状态分离,并自动执行了很多操作,比如重新获取和缓存。”Zac 解释道。

我立刻就想到了这个想法:React 应用在 store 中保存的大多数状态只是远程持久化数据的映射(例如用户、帖子列表、评论或待办事项)。只有一小部分状态仅在客户端,并且几乎总是与 UI/UX 信息相对应,例如模态框是否打开、侧边栏是否展开等等。

React Query 背后的理念是将大部分服务器端状态全部处理:抓取、重新抓取、存储、缓存、更新和记忆,都整合到一个一体化解决方案中。这种分离有助于减少其他客户端和服务器端状态管理工具(例如 Redux)中不可避免的大量样板代码。

该库还提供了一些高级功能,例如“乐观更新”,其中库假设在实际从后端接收响应之前对数据的更新将会成功,并且如果失败则允许轻松回滚,使应用程序对用户来说似乎响应轻而易举。

前景光明。我们决定在应用的概念验证阶段就采用它,并开始编写代码。

使用 create-react-app 编写 PoC

当我们在后端团队有能力构建提供应用程序所需数据的服务之前就开始着手前端工作时,我们决定继续使用create-react-app 及其 TypeScript 模板和使用 JSONPlaceholder 作为虚假 API 的 React Query 来设置我们的项目。

那么,让我们编写一些代码吧!

首先,我们使用 create-react-app 的 CLI 创建了一个新应用程序并安装了 react-query:

npx create-react-app react-query-demo --template=typescript
cd react-query-demo
yarn add react-query
Enter fullscreen mode Exit fullscreen mode

App.tsx默认的组件如下所示

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

按照 React-Query 的优秀文档,我们首先通过QueryClientProvider使用库中包含的包装我们的应用程序来修改该文件,并创建一个新组件,UserList我们将从Users我们的虚假 API 中获取我们的组件。

import React from 'react';
import { QueryClientProvider, QueryClient } from 'react-query';

import './App.css';
import { UserList } from "./UserList"

const queryClient = new QueryClient();

const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <header className="App-header">
          <h1>React Query Demo</h1>
        </header>
        <UserList />
      </div>
    </QueryClientProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

让我们在该组件中解压更改。首先,我们使用React Query 提供的构造函数实例化了一个新queryClient实例。然后,我们将该实例传递给我们用来包装整个应用的 。这为我们的缓存数据提供了一个上下文,并允许所有包装在其中的组件使用该库提供的查询和更新钩子。QueryClientQueryClientProvider

我们还稍微清理了一下组件,修改了标题,并添加了新创建的UserList组件,事情开始变得真正有趣了。我们来看看:

import React from "react";
import { useQuery } from "react-query";

interface User {
  id: number;
  name: string;
  username: string;
  email: string;
}

const USERS_KEY = "users";

export const UserList = () => {
  const {
    isLoading,
    data: users,
    isError,
    error
  } = useQuery<User[], Error>(
    USERS_KEY,
    () => fetch('https://jsonplaceholder.typicode.com/users')
  ).then(res => {
    if (!res.ok) {
      throw new Error('Network response failed')
    }
    return res.json()
  }));

  if (isLoading) {
    return <span>Loading...</span>;
  }

  if (isError) {
    return <span>Error: {error?.message}</span>;
  }

  return (
    <ul>
      {users?.map(({ name, username, email }: User) => (
        <div className="userRow">
          <h3>{name}</h3>
          <p>Username: {username}</p>
          <p>{email}</p>
        </div>
      ))}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

这里还有很多内容,但这才是 React Query 真正展现其精髓的地方。让我们来一探究竟。

由于我们使用 JSONPlaceholder 的伪 API 来获取用户列表,因此我们首先创建User一个基于网站提供的架构的简化版本界面。在本例中,我们将获取一个用户数组并将其显示给用户。

在我们的组件中,我们使用了 React-Query 提供的主要工具:useQueryhook。hook 接受两个参数:

  • 一个唯一的查询键,React Query 在内部使用它来“在应用程序内重新获取、缓存和共享查询”。库会将数据存储在这个键下,就像 Redux 中不同 Reducer 的数据保存在一个键名下一样。在我们的例子中,我们将其设置为一个USERS_KEY常量,它只是一个 value 的字符串"users"
  • 返回解决数据的承诺或引发错误的函数

第二个参数凸显了该库的一大优势:由于 React Query 的获取机制是基于Promises构建的,因此它可以与任何异步数据获取客户端一起使用,例如 Axios、原生fetch甚至 GraphQL!(我们将在后续文章中详细介绍如何做到这一点)。

目前,我们使用fetchUser从端点请求一个 s 列表https://jsonplaceholder.typicode.com/users。需要注意的是,使用 fetch 时,我们还必须手动检查请求是否成功,如果失败则抛出错误,因为第二个参数期望 fetcher 函数在发生错误时抛出错误,而 fetch 不会自动执行此操作。例如,如果我们使用 Axios,则无需这样做。

TypeScript 用户请注意: React Query 允许你通过泛型提供其 hooks 的结果和错误类型。这在创建自定义 hooks 时尤其有用,例如:

const useGetUsers = () => {
   return useQuery<User[], Error>('users', fetchUsers)
}
Enter fullscreen mode Exit fullscreen mode

useQuery钩子返回一个对象,我们从中解构了三个属性:

  • isLoading:一个布尔值,表示查询没有数据并且当前正在获取。
  • data:该属性包含 Promise 在请求成功时解析的数据。在我们的例子中,它是一个 s 数组,为了清晰起见,User我们将其别名为变量名。users
  • isError:一个布尔值,表示查询遇到错误。
  • error:如果查询处于isError状态,则包含抛出的错误的属性

我们可以使用这些属性来根据查询的状态决定组件应该渲染什么。首先,我们检查组件是否处于某个isLoading状态,并相应地渲染一条消息。然后,我们通过布尔值检查组件是否发生了错误isError,并在 下显示错误error.message。最后,我们可以安全地假设查询处于该isSuccess状态并渲染用户列表。

更新服务器端状态

到目前为止一切都很好,但是当我们需要创建、更新或删除远程存储的数据时该怎么办?React Query 使用Mutations和hook的概念解决了这个问题useMutation

让我们创建另一个组件CreateUser,该组件呈现一个按钮,单击该按钮时会将新用户发布到 API,并将其添加到我们的App.

[...]

const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <header className="App-header">
          <h1>React Query Demo</h1>
        </header>
        <UserList />
                <CreateUser />
      </div>
    </QueryClientProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

这次,我们将使用 Axios 作为 HTTP 客户端,以突出 React Query 的多功能性。我们先安装它:

yarn add axios
Enter fullscreen mode Exit fullscreen mode

让我们为新组件编写代码:

import React from "react";
import axios from "axios";
import { useMutation, useQueryClient } from "react-query";

import { User, USERS_KEY } from "./UserList";

const exampleUser = {
  name: "John Doe",
  email: "johndoe@gmail.com",
  username: "johndoe1990"
} as User;

const postUser = (user: User) => axios
    .post<User>('https://jsonplaceholder.typicode.com/users', user);

export const CreateUser = () => {
  const queryClient = useQueryClient();
  const { isLoading, mutate } = useMutation(postUser, {
    onSuccess: () => {
      queryClient.invalidateQueries(USERS_KEY);
    }
  });
  const onButtonClick = () => mutate(exampleUser);

  if (isLoading) {
    return <p>Creating User...</p>;
  }

  return <button onClick={onButtonClick}>Click to post a new user</button>;
};
Enter fullscreen mode Exit fullscreen mode

让我们回顾一下这里发生的事情。

首先,我们创建一个硬编码的exampleUserPOST 函数,用于在用户点击按钮时将请求发送到伪造的 API。我们还创建了所需的变异函数postUser 它将一个 Axios 响应的 Promise 返回到我们的/users端点,并将变异函数的参数作为数据传入。

在组件内部,我们首先使用 React Query 提供的钩子初始化一个 实例queryClient。这与我们在 中创建并提供的useQueryClient实例相同。我们稍后会使用它。App.tsxQueryClientProvider

现在我们使用useMutationReact Query 提供的钩子,它接受两个参数:

  • 一个必需的突变函数,它执行异步任务并返回 Promise。在我们的例子中,我们传入的是已经定义的postUser函数。
  • 具有多个属性的对象:
    • 一个可选的变异键 (mutation key),其定义方式与我们定义的查询键 (query key)类似,供内部使用。本例中我们无需设置。
    • 可选的onSuccess回调,当突变成功并传递突变结果时触发。
    • 如果突变失败,将触发可选的onError回调,并将传递错误。
    • 一个可选的onMutate回调,它在突变函数触发之前触发,并传递与突变函数相同的变量。这使我们能够进行乐观更新:也就是说,我们可以提前更新资源(以及我们的 UI),希望突变能够成功,并赋予我们的应用一种“同步的感觉”。此函数的返回值将传递给onErroronSettled回调,以便在突变失败时回滚我们的乐观更新。
    • 可以在文档中找到更多配置属性。

在我们的示例中,我们仅设置了一个onSuccess回调函数,其作用是使查询无效"users",方法是调用invalidateQueries我们提供的实用程序queryClient并将我们的 as 参数传递USERS_KEY给它。通过在变更成功后使缓存中的此查询键无效,我们向 React Query 指示该键下的数据已过期,应该重新获取。因此,该库将自动重新查询我们的/users端点,并返回更新后的Users列表。

useMutation钩子返回一个对象,我们从中解构两个属性:

  • mutate:一个可以通过将变量作为参数传递给它来调用的函数,它将触发钩子中定义的突变函数中定义的突变。
  • isLoading:一个布尔值,表示突变仍处于待处理状态。

我们的CreateUser组件会在点击按钮时使用mutate 方法onButtonClick,因此我们创建一个函数,将mutate硬编码的参数exampleUser作为参数传递给它。然后,我们使用isLoading标志位,在突变待处理时向用户显示相应的消息,否则显示带有号召性用语的按钮。

就这样!试着在应用中尝试一下。不过需要注意的是,如果你查看 DevTools 上的“网络”选项卡,你会发现,由于我们使用的是伪 API,添加用户的POST201调用确实会成功,状态码为。然而,当 React Query 重新获取数据时(在我们使查询键无效后触发的后续GET调用),新用户将不会出现在返回的数据中,因为JSONPlaceholder会直接忽略任何添加到其中的新数据。然而,在真正的 API 上,你会看到User你刚刚发布的 。

总结

我们已经了解了 React Query 如何处理数据的获取、缓存和更新(通过重新获取),并提供了一个用户友好的钩子来处理返回的数据和渲染。通过使用其查询键和简单的 API,该库可以取代成熟的状态管理解决方案,让您免于编写数百行样板代码的负担,并添加一些原本需要从头开始编写的强大功能。

查看已完成的演示应用,并克隆代码库来试用代码。别忘了阅读官方文档

感谢阅读!

鏂囩珷鏉ユ簮锛�https://dev.to/juandj/using-react-query-to-solve-state-management-in-your-react-app-4kf9
PREV
将 Node.js 微服务部署到 ZEIT Now
NEXT
React 键的意义——视觉解释