React 状态管理全景对比:Context、Redux、Zustand 与 Jotai

引言

「React 状态管理选型」是每个前端团队都绕不开的决策。从早期的 Flux、Redux,到中期的 MobX,再到近年的 Zustand、Jotai、Valtio,方案层出不穷。本文不站队任何库,而是从状态的生命周期、粒度、读写模式三个维度建立分析框架,再逐一拆解主流方案的设计哲学与适用场景,最后给出决策流程图。

一、状态分类:先想清楚要管理什么

很多选型争论其实源于「在讨论不同的状态」。按生命周期与作用域,React 状态可分为五类:

flowchart TB
  classDef s1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef s2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef s3 fill:#e0f7fa,stroke:#00838f,color:#004d40
  classDef s4 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
  classDef s5 fill:#fff3e0,stroke:#f57c00,color:#e65100

  A[React 应用状态全谱] --> B[本地状态<br/>useState/useReducer<br/>组件生命周期内]
  A --> C[跨组件状态<br/>Context / 状态库<br/>兄弟/深层组件共享]
  A --> D[服务端状态<br/>请求缓存/数据同步<br/>TanStack Query 等]
  A --> E[URL 状态<br/>路由参数/搜索条件<br/>可分享可回退]
  A --> F[全局配置<br/>主题/用户信息<br/>应用启动即加载]

  click C "https://react.dev/learn/passing-data-deeply-with-context" "Context 传递数据"
  click D "https://tanstack.com/query/latest" "TanStack Query"
  class A s1
  class B s2
  class C s3
  class D s4
  class E s5
  class F s2

选型第一原则:能用 useState 解决的不上状态库;能提升到父组件的不引入全局;服务端数据一律走缓存类方案(TanStack Query / SWR),不要塞进本地状态库。

二、原生方案的演进:Context 与 useReducer

在引入第三方库之前,先评估 React 内置能力是否足够。

Context 的工作原理与局限

Context 提供「跨层级传递数据」的能力,避免逐层 props 透传。但 React 官方文档明确提醒:Context 不是状态管理工具,它只解决「传递」问题,不解决「更新」问题。当 Context 值变化时,所有消费该 Context 的组件都会重新渲染——无论它们是否用到了变化的部分。

flowchart TB
  classDef c1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef c2 fill:#ffebee,stroke:#c62828,color:#b71c1c
  classDef c3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20

  A[App 提供 AuthProvider] --> B[Provider value 变更]
  B --> C{Context 消费者}
  C --> D[组件 X 只读 user.name]
  C --> E[组件 Y 只读 token]
  C --> F[组件 Z 不消费]
  B --> G{重新渲染范围}
  G --> H[X 重渲染 不需要也渲染]
  G --> I[Y 重渲染]
  G --> J[Z 不受影响]

  click B "https://react.dev/learn/scaling-up-with-reducer-and-context" "Reducer 与 Context 组合"
  class A c1
  class B c1
  class C c1
  class D c3
  class E c3
  class F c2
  class G c1
  class H c2
  class I c3
  class J c3

Context 的性能优化手法

  1. 拆分成多个 ContextThemeContextUserContext 分离,避免「改主题重渲染整个用户区」;
  2. 值与 setter 分离ValueContext + SetterContext,让只写组件不订阅值;
  3. 用 memo 包裹消费组件useContext 返回值变化时,仍可配合 React.memo 隔离子树。

useReducer + Context 的组合模式

当跨组件状态具备「多步操作、相互依赖」的特点时,useReducer + Context 是官方推荐的零依赖方案:

flowchart LR
  classDef r1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef r2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef r3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[dispatch action] --> B[Reducer 纯函数]
  B --> C{匹配 action.type}
  C -->|ADD_TODO| D[返回新数组]
  C -->|TOGGLE_TODO| E[映射更新]
  C -->|DELETE_TODO| F[过滤删除]
  C -->|default| G[返回原状态]
  D --> H[Context 值更新]
  E --> H
  F --> H
  G --> H
  H --> I[消费组件重渲染]

  click B "https://react.dev/reference/react/useReducer" "useReducer 文档"
  class A r1
  class B r1
  class C r2
  class D r3
  class E r3
  class F r3
  class G r3
  class H r2
  class I r2

适用边界:中等规模、更新频率低、无复杂派生、无跨标签页同步。超过这个边界,就该考虑专门的库。

三、Redux Toolkit:约定最重的重量级选手

Redux 的核心是单一 store + 纯 reducer + 单向数据流,配合官方推荐的 Redux Toolkit(RTK)与 RTK Query,其工程化程度仍是大型团队的首选。

单向数据流

flowchart TB
  classDef d1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef d2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef d3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[UI 组件] --> B[dispatch action]
  B --> C[Reducer 纯函数]
  C --> D[新 state]
  D --> E[store 持有]
  E --> F[订阅者通知]
  F --> G[selector 选取切片]
  G --> H[组件重渲染]

  click C "https://redux-toolkit.js.org/usage/usage-guide" "RTK 使用指南"
  class A d1
  class B d1
  class C d2
  class D d2
  class E d1
  class F d1
  class G d3
  class H d3

RTK 的核心优势

能力 说明 解决的问题
createSlice reducer + actions + 类型一体化 样板代码爆炸
Immer 集成 直接「修改」state 对象 不可变更新的心智负担
RTK Query 服务端缓存/轮询/乐观更新 数据请求管理
DevTools 时间旅行调试 复杂状态回放排查
TypeScript 优先 全链路类型推导 大型团队协作安全

何时选择 Redux

flowchart TD
  classDef p1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef p2 fill:#fff3e0,stroke:#f57c00,color:#e65100
  classDef p3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20

  A[应用是否足够复杂] --> B{多团队多人协作}
  B -->|是| C[Redux 规范约束强]
  B -->|否| D{状态更新是否高频高并发}
  D -->|是| E[考虑 Zustand 更轻]
  D -->|否| F{是否需要时间旅行调试}
  F -->|是| G[Redux DevTools 最成熟]
  F -->|否| H[Context + useReducer 即可]

  click G "https://redux-toolkit.js.org/usage/nextjs" "RTK 集成框架"
  class A p1
  class B p1
  class C p3
  class D p2
  class E p2
  class F p2
  class G p3
  class H p3

四、Zustand:极简主义的现代选择

Zustand(德语「状态」)以「小而美」著称:无需 Provider 包裹、无样板代码、选择器级订阅,配合 create 一行即可创建 store。

Zustand 的核心机制

flowchart LR
  classDef z1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef z2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef z3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[create 定义 store] --> B[外部模块级单例]
  B --> C{选择器订阅}
  C --> D["useStore 选择器<br/>精确订阅切片"]
  C --> E["useStore 选择器<br/>引用稳定不重渲染"]
  D --> F[切片变化才重渲染]
  B --> G[非 React 环境调用<br/>getState/setState]

  click D "https://zustand.docs.pmnd.rs/guides/using-slices" "Zustand 切片模式"
  class A z1
  class B z2
  class C z1
  class D z3
  class E z2
  class F z3
  class G z2

Zustand 的设计亮点

  1. 无 Provider:store 是普通模块变量,可在组件外任意调用;
  2. 选择器订阅:默认按引用比较,切片不变则不重渲染;
  3. 中间件生态persist(持久化)、devtoolsimmersubscribeWithSelector
  4. Store 拆分create()(...) 多次创建独立 store,避免巨型单例。

典型误区:在组件里写 useStore()(不带选择器)等于订阅整个 store,任何字段变化都会重渲染,性能反而不如 Context。

五、Jotai:原子状态模型

Jotai 把状态拆成最小的「原子」(Atom),通过依赖图自动派生、自动追踪订阅,适合「状态粒度极细」的场景。

原子派生模型

flowchart TB
  classDef j1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef j2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef j3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[atom 基础状态<br/>inputValue] --> C[派生原子<br/>computedResult]
  B[atom 基础状态<br/>filter] --> C
  C --> D[异步原子<br/>atomWithQuery]
  D --> E[组件订阅 useAtom]
  E --> F[依赖图自动更新]

  click A "https://jotai.org/docs/basics/atoms" "Jotai 原子概念"
  class A j1
  class B j1
  class C j2
  class D j2
  class E j3
  class F j2

Jotai vs Zustand 的选择:Zustand 适合「共享状态较少、以模块为单位组织」;Jotai 适合「状态颗粒细碎、派生关系复杂」——如表单生成器、可视化配置面板。

六、服务端状态:另一类问题的解法

服务端状态(来自 API 的数据)是 React 应用中最容易被误管理的状态。它的语义与本地状态完全不同:需要缓存、失效、重试、乐观更新、分页、无限滚动……

TanStack Query 的缓存生命周期

sequenceDiagram
  participant C as 组件
  participant Q as Query 缓存
  participant A as API

  C->>Q: 请求数据(useQuery)
  Q->>Q: 检查缓存命中
  Q-->>C: 命中则返回缓存(stale 时后台刷新)
  Q->>A: 未命中发起请求
  A-->>Q: 响应写入缓存
  Q-->>C: 返回数据并通知
  C->>Q: 用户操作触发失效
  Q->>Q: 标记 stale 重取

状态管理决策全景图

flowchart TD
  classDef f1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef f2 fill:#fff3e0,stroke:#f57c00,color:#e65100
  classDef f3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20
  classDef f4 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c

  A[需要管理的状态] --> B{是否来自 API}
  B -->|是| C[TanStack Query / SWR]
  B -->|否| D{是否需要跨组件共享}
  D -->|否| E[useState / useReducer]
  D -->|是| F{状态量级}
  F -->|小| G{更新频率}
  G -->|低| H[Context 拆分子树]
  G -->|高| I[Zustand 或 Jotai]
  F -->|大| J{团队与规范需求}
  J -->|强规范多协作| K[Redux Toolkit]
  J -->|轻量敏捷| I

  click C "https://tanstack.com/query/latest" "TanStack Query"
  click K "https://redux-toolkit.js.org/" "Redux Toolkit"
  class A f1
  class B f1
  class C f3
  class D f1
  class E f3
  class F f2
  class G f2
  class H f3
  class I f3
  class J f4
  class K f3

七、总结与选型建议

  1. 默认策略:本地状态 useState,跨层传递 Context(拆分值与 setter),复杂更新 useReducer
  2. 服务端数据:一律交给 TanStack Query / SWR,不要在状态库中复制服务端数据;
  3. 中型应用:Zustand 的极简哲学 + 选择器订阅,性价比最高;
  4. 大型协作应用:Redux Toolkit 的强约束 + DevTools 时间旅行,长期维护更稳;
  5. 颗粒状态:Jotai 原子模型,派生计算自动追踪;
  6. 注意迁移成本:方案可以换,状态边界设计是灵魂——先画状态分类图,再选工具。

最后送大家一句话:状态管理的关键不是「选哪个库」,而是「哪些状态根本不需要库」。克制,是最好的架构。