首页 / 文章 / React Query与Redux:重新思考大型应用中的服务器状态管理

React Query与Redux:重新思考大型应用中的服务器状态管理

了解为何某款生产级聊天应用选择使用 TanStack Query 而非 Redux 来管理服务器数据,以及 Redux 在现代 React 架构中仍能发挥作用的场景。

2701 词

想象一下,有位开发者从规模较小的项目转向一家以产品为核心的公司,他迫切想了解大规模、生产级软件究竟是如何构建的。在多年从事前端应用开发之后,加入一个负责为数百万人提供服务的软件团队,会让他以不同的方式来评估代码。

他不再只问“这个功能能用吗?”

取而代之的是,他会开始思考:

这种架构在数百万用户的使用下表现如何? 整个应用中的状态是如何保持同步的? 当十个不同的组件都需要同一份数据时会发生什么? 随着产品不断发展,工程师们如何让如此庞大的代码库依然易于维护?

现在再假设这位开发者被分配到产品的核心模块之一——一个类似Slack的聊天应用来负责开发。

这并非隐藏在应用某个角落的次要功能。

聊天体验处于产品的核心位置,服务于数百万用户,并与关键的业务工作流程、创造收入的业务环节以及公司一些最重要的企业客户紧密相连。

因此,这位开发者在首次深入研究代码库时,会有相当可预见的预期也是合情合理的。

如此规模的聊天应用不可避免地要处理海量信息,这些信息需要通过多个界面进行共享:对话线程及其消息、未读消息数量、参与对话的人员、历史记录的浏览、编辑及其他写入操作、加载中指示器,以及实时到达的更新内容。

考虑到这一切,在一个庞大的 React 代码库中,人们自然会期待看到那些常见的元素:

Redux、Zustand,或者至少某种形式的全局状态管理。

但在仔细查看代码后,却找不到任何状态存储机制。

没有 Redux 的相关配置。

也没有 Zustand。

更没有用于存储应用共享数据的庞大 Context 对象。

乍看之下,很容易以为在浏览代码库时遗漏了什么。

但进一步深入后,就会发现真正驱动共享数据层的是什么。

TanStack Query。

如果你对这个库的认知较为有限,这确实是个令人惊讶的发现。

许多开发者认为 React Query 主要只是用于获取 API 数据、缓存响应、跟踪加载状态,以及在数据发生变化时重新获取数据的工具。

然而,在这个服务于数百万用户的生产级应用中,查询缓存的作用远不止于此。它实际上充当了整个应用中所有服务器持有数据的共享存储空间

这一发现挑战了长期以来关于前端架构的固有假设。

也许正确的问题并非:

“为什么这里没有 Redux 存储?”

或许应该是:

“为什么服务器持有的数据一开始就非得存在 Redux 中?”

这个问题引发了更深入的探讨,让我们思考如何在 React 应用中构建状态管理机制,以及为何在许多实际系统中,TanStack Query 能够轻松取代团队们过去手动构建的大量全局状态管理组件。

从 Redux 到 React Query

曾有一段时间,将 API 调用集成到 React 应用中似乎是一件很简单的事。

后来 Redux 出现了。

API 调用本身依然简单,但围绕它的所有内容却变得复杂起来。

你需要定义一个动作。

编写一个还原器。

添加加载状态和错误提示。

派发该动作。

将响应保存到状态存储中。

编写一个选择器来读取这些数据。

再将组件与这一切连接起来。

几周或几个月后,总有人会问:

“为什么这些数据看起来过时了?”

而解决办法通常又是创建另一个动作来强制重新获取数据。

经过多次这样的循环后,一个明显的不良模式便显现出来:

全局状态工具常常被用来管理那些从一开始就并非客户端状态范畴的问题。

后端才是数据的真正拥有者。

前端仅仅负责使用这些数据。

正是这种区别使得 TanStack Query 成为现代 React 应用中一种极具吸引力的替代方案。

首先,React Query 到底是什么?

在探讨它如何减少对 Redux 的需求之前,有必要先澄清一个常见的误解。

TanStack Query(原名 React Query)并非 useState、Redux 或 Zustand 的替代品。

它的核心职责是管理服务器状态——即那些源自 React 应用之外、需要被获取、缓存、保持同步、更新,最终会被视为过时数据的状态。

React Query 并不仅仅是发送请求并将响应放入组件的本地状态中这么简单。

TanStack的官方文档将该库的功能明确定义为获取、缓存、同步以及更新服务器状态。

可以把数据理解为:

Users
Projects
Messages
Notifications
Orders
Analytics

前端实际上并不拥有这些信息。

后端才拥有它们。

React Query充当了用户界面与后端之间的桥梁,负责管理这些数据的生命周期。

这一设计的核心是查询缓存

根据TanStack当前的文档说明,QueryCache是用于存储查询结果的层——其中保存着数据、元数据以及状态信息。QueryClient负责管理这个缓存,并提供应用程序用来读取数据、更新数据、使条目失效以及进行其他操作的接口。

这正是为什么应用程序中多个互不相关的部分可以发起相同的查询并得到一致结果的原因:

const { data } = useQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects
})

不过,React Query的功能远不止缓存单个响应。

它将缓存、请求去重、数据新鲜度跟踪、后台重新获取数据、重试逻辑、垃圾回收、数据变更以及无效化处理全部整合在同一个系统中。

例如,在项目更新之后:

const queryClient = useQueryClient()
await updateProject(project)
queryClient.invalidateQueries({
  queryKey: ['projects']
})

无需手动指导十个独立的组件如何更新它们本地的项目数据,只需告知查询系统:

“此查询对应的服务器数据可能已不再准确。”

之后由缓存系统负责重新验证这些数据。

这就是TanStack Query的核心理念。

它并非试图成为第二个Redux。

这赋予了服务器状态独立的生命周期。

一旦你开始将服务器状态视为与客户端状态有本质区别,就很容易理解:那些表面上看似需要庞大 Redux 存储的应用其实可能根本不需要它。

Redux 从来都不是问题所在

公平地说,Redux 本身并无过错。

Redux Toolkit 依然是编写 Redux 应用的官方推荐方式,而当应用确实需要复杂的客户端状态管理、可预测的状态转换、中间件流程或统一的州模型时,Redux 依然能发挥重要作用。

问题出现在开发者将所有内容都塞进同一个全局存储中时。

想象这样一个应用结构:

{
  user: {},
  projects: [],
  teams: [],
  notifications: [],
  orders: [],
  products: [],
  analytics: {},
  theme: "dark",
  sidebarOpen: true
}

乍看之下,这一切似乎都属于该应用程序。

但实际上并非如此。

试着问一个简单的问题:

谁才是这些数据的真正所有者?

项目列表是由 React 应用程序控制的吗?

并非如此。

真正的所有者是后端。

当你的浏览器标签页保持打开状态时,是否有其他用户可以修改订单?

当然可以。

是否有可能在你的 React 代码没有执行任何操作的情况下出现通知?

绝对有可能。

服务器是否可以单方面撤销或更改用户的权限?

毫无疑问。

因此,那部分“应用程序状态”其实根本不属于前端所有。

这就是服务器状态

而服务器状态会带来完全不同类别的挑战。

你必须获取它。

你必须对它进行缓存。

你必须判断它何时失效。

你必须重新获取它。

在数据发生变更后,你必须保持它的同步。

你必须处理加载指示器和错误状态。

你必须考虑重试以及网络连接中断的问题。

这正是TanStack Query被设计用来解决的那些问题。

架构上的转变

传统的以 Redux 为中心的架构通常遵循这样的流程:

API
 ↓
Async action / thunk
 ↓
Reducer
 ↓
Redux Store
 ↓
Selector
 ↓
React Component

对于许多基于 API 的应用,这种流程可以被调整为类似这样的形式:

API
 ↓
TanStack Query
 ↓
Query Cache
 ↓
React Component

从理论上看,两者的差异可能似乎很小。

其实并非如此。

真正的变化在于,你不再需要手动构建服务器状态所需的所有相关组件。

就拿项目列表这种常见例子来说吧。

在 Redux 中,通常的实现方式是:

const initialState = {
  data: [],
  loading: false,
  error: null
}

接着添加一个异步动作:

dispatch(fetchProjects())

随后是处理相关逻辑的 reducer:

pending
fulfilled
rejected

最后是一个选择器:

const projects = useSelector(
  state => state.projects.data
)

现在将这种方式与 TanStack Query 的版本进行对比:

const { data, isPending, error } = useQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects
})

这不仅仅是代码行数的减少。

查询本身成为了封装服务器资源的抽象层。

查询键用于指定正在跟踪的资源。

缓存会保存查询结果。

查询还能监控自身的状态。

而且任意数量的组件都可以从同一个缓存值中获取数据。

TanStack Query 的 QueryCache 专门用于存储查询结果及其相关状态,而 QueryClient 则提供了操作该缓存的接口。

查询缓存本质上是服务器数据的全局存储

这可能是这里最重要的概念。

很多开发者听到这样的说法:

“React Query 有缓存。”

便认为其含义是:

“它只是用来缓存 API 响应而已。”

但实际上它的功能远不止于此。

查询缓存实际上成了应用程序服务器状态的共享且唯一的真实数据源。

想象有三个独立的组件:

Dashboard
   |
   +── ProjectList
   |
   +── ProjectSidebar
   |
   +── RecentProjects

它们每一个都需要相同的数据,这些数据以如下方式标识:

['projects']

无需先手动将服务器数据导入 Redux,再让三个组件都从存储中读取数据。

只需在需要之处发起相同的查询即可:

useQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects
})

TanStack Query 会在后台负责数据的共享与缓存。

最终的架构可能呈现为如下形式:

               React App
                  |
        ┌─────────┴─────────┐
        |                   |
   Client State        Server State
        |                   |
 Redux / Zustand       TanStack Query
        |                   |
     UI state         Query Cache

这样一来,整个系统的理解难度就大大降低了。

那么 React Query 能取代 Redux 吗?

在某些应用中,答案确实是肯定的。

但关键在于:

它并非通过成为 Redux 的更优版本来取代 Redux。

相反,它是消除了最初就依赖 Redux 来管理服务器状态的需求。

这其实是两个截然不同的概念。

TanStack Query的官方文档明确将服务器状态管理视为与客户端状态管理不同的问题,并指出一旦服务器状态被移交给React Query,那些仍需全局管理的客户端状态就可以大幅减少。

从架构角度来看,这时事情才真正变得有趣起来。

你可能会采用这样的分离方式:

Client State
theme
sidebar
selectedTab
modal
editor
filters

与此同时:

Server State
users
projects
orders
notifications
products
analytics

此时你的工具选择就会更加有针对性:

Client State → Redux / Zustand / Context / React
Server State → TanStack Query

而不是期望一个存储同时承担这两项任务。

但Redux仍有用武之地

考虑另一种类型的应用程序,比如类似Figma这样的设计工具。

你可能需要存储如下状态:

{
  selectedLayer,
  activeTool,
  zoom,
  canvasMode,
  dragState,
  undoStack,
  redoStack
}

以上内容均不属于服务器状态。

它们完全由前端掌控。

这些状态会随着用户操作的实时反馈而同步变化。

界面的多个部分会同时依赖这些状态。

当某个状态发生变化时,可能还需要协调复杂的过渡效果。

正是在这种场景下,专门的客户端状态管理器才显得十分重要。

TanStack Query的文档也提到了类似观点:那些与服务器无关、且较为复杂的、由界面驱动的状态,依然需要使用专为处理此类状态而设计的客户端工具。

因此,正确的做法并非“到处都用React Query取代Redux”,那样做未免矫枉过正。

还有另一个重要工具:RTK Query

还有一个值得提及的复杂因素。

Redux Toolkit 已内置 RTK Query,这是一个专为在 Redux 应用程序中使用的数据获取与缓存层。

它能像 TanStack Query 一样自动生成钩子,并处理接口请求、加载状态以及缓存功能。

因此,该生态系统正在发生的真正变化并非仅仅是:

Redux → React Query

实际情况更像是这样的发展过程:

Manual API state in Redux
          ↓
Dedicated server-state solutions
          ↓
TanStack Query / RTK Query / Apollo / SWR

整个行业正在逐渐意识到服务器状态与客户端状态是本质不同的职责,需要使用不同的工具来处理。

一旦明确了这一区别,整个状态架构就会变得更容易理解。

我现在的准则

在判断某段数据是否应放入全局状态时,有一个问题就能解决大部分问题:

谁才是这些数据的真正所有者?

如果答案是:

后端

那几乎可以肯定你正在处理服务器状态。

如果答案是:

前端

那几乎可以肯定你正在处理客户端状态。

根据不同情况,这个简单的问题往往能指引你采用截然不同的架构。

例如:

Current user ────────── Server
Projects ────────────── Server
Orders ──────────────── Server
Notifications ───────── Server
Theme ───────────────── Client
Modal ───────────────── Client
Selected tab ────────── Client
Editor state ────────── Client

一旦以这种方式来梳理,合适的结构就会变得显而易见。

React Query 并不会取代 Redux

“React Query 正在取代 Redux”这一说法其实有些误导。

真正发生变化的是更微妙的东西:

开发者们越来越擅长判断自己所处理的究竟属于哪一类状态。

Redux 曾经是所有数据的默认存储处,包括服务器响应。

越来越多的团队开始进行分离:

Server state
        ↓
TanStack Query

来自:

Client state
        ↓
Redux / Zustand / Context / React

对于许多现代 React 项目来说,仅这一分离就能消除相当一部分原本归咎于 Redux 自身的复杂性。

重点不在于减少使用的库的数量。

关键是要停止为那些已有完善专用抽象层的功能手动构建基础设施。

所以下次当你遇到一个充斥着 API 响应、加载状态标志、缓存失效逻辑以及重新获取操作的臃肿 Redux slice 时,问问自己:

你真的需要一个全局状态管理器来解决这个问题,还是只是在 Redux 内部手动重新实现了 React Query?

你在自己的应用中是如何处理这个问题的?

您目前是将服务器数据存储在 Redux 或 Zustand 中,使用 TanStack Query,还是采用完全不同的方法?

相关阅读

  • 了解 React 自定义钩子:无需共享状态即可复用逻辑 — 了解什么是 React 自定义钩子,它们如何在不同组件之间提取和共享带状态的逻辑,以及在构建时需要避免哪些常见错误。
  • Power Apps 与 React:长期成本与架构对比 — 本文分析了决定 Power Apps 或 React 在大规模应用中谁更便宜的隐性许可成本、架构权衡以及管理现实问题。
  • 重新思考 React 状态:数据应存储在何处 — 本文介绍了如何通过将状态放在 URL、DOM 或派生值中,而非过度使用 useState,从而减少 React 中的错误。