React 项目发展到一定规模以后,状态管理一定会变成架构问题。它不再只是“组件之间怎么共享一个值”,而是要回答几个更具体的问题:
这份状态来自服务端还是客户端它需要被多少页面共享它变化的频率有多高它会不会影响大量组件渲染它是否需要缓存、失效、回滚和同步团队能不能稳定维护这套约定
所以比较状态管理库时,不能只看 API 简单不简单,也不能只看社区热度。更合理的方式是先拆开状态类型,再看每个工具解决的是哪一类问题。
先区分服务端状态和客户端状态
很多状态管理方案选错,并不是因为库不好,而是因为把不同类型的状态混在了一起。
服务端状态通常来自接口,例如项目详情、任务列表、成员权限、文章内容、订单信息。它的特点是:数据源不在浏览器里,需要请求、缓存、刷新、失效、重试,还要处理多人协作或后端变更带来的不一致。
客户端状态则是浏览器内产生的状态,例如弹窗开关、当前选中的 Tab、侧边栏是否收起、表格筛选条件、跨页面临时选择结果。它的特点是:数据源就在前端,更关注更新范围和交互响应。
这两类状态应该分开治理:
| 状态类型 | 典型工具 | 核心问题 |
|---|---|---|
| 服务端状态 | TanStack Query、SWR、RTK Query | 请求、缓存、失效、同步 |
| 客户端全局状态 | Zustand、Redux Toolkit、MobX、Jotai、Valtio | 共享、更新、派生、订阅 |
| 局部状态 | useState、useReducer、Context | 组件内或局部树内协作 |
如果一个项目里大量接口数据都被塞进全局 store,那么无论用 Redux、MobX 还是 Zustand,后面都会遇到缓存过期、重复请求和数据一致性问题。
Redux Toolkit 适合强约束的大型团队
Redux 早期的问题是模板代码多,写一个简单状态也需要 action type、action creator、reducer 和 store 配置。Redux Toolkit 解决了很多历史包袱,提供了更标准的写法,也成为现在使用 Redux 的默认方式。
它的优势是约束强、生态成熟、调试能力好。状态变化通过 action 进入 reducer,整体链路非常明确。大型团队里,如果需要统一数据流、统一调试方式、统一异步处理规范,Redux Toolkit 仍然是可靠选择。
const counterSlice = createSlice({ name: "counter", initialState: { value: 0 }, reducers: { increment: state => { state.value += 1; } }});
它的缺点也来自这套强约束。即使有 Toolkit,Redux 仍然需要围绕 slice、dispatch、selector、middleware 建立一套工程规范。对于中小型项目或者状态边界比较简单的项目,Redux 可能显得偏重。
适合 Redux Toolkit 的场景:
- 团队规模较大,需要非常统一的状态规范。
- 状态变化需要完整追踪和调试。
- 业务有复杂的全局客户端状态。
- 项目已经深度使用 Redux 生态。
- 服务端状态也希望通过 RTK Query 接入同一体系。
不适合的场景是:只是想管理几个弹窗、筛选条件和页面偏好。此时引入 Redux 往往会让架构比问题本身更重。
MobX 适合对象模型清晰的响应式业务
MobX 的核心优势是响应式自然。开发者可以像操作普通对象一样修改状态,依赖这些状态的视图会自动更新。它适合领域模型比较清晰、对象之间关系比较明确的业务。
class TodoStore { todos = []; constructor() { makeAutoObservable(this); } get unfinishedCount() { return this.todos.filter(todo => !todo.done).length; }}
MobX 的表达力很强,observable、computed、action 组合起来,可以很自然地描述业务对象和派生状态。
但 MobX 最大的问题是隐式依赖。组件在渲染时读到了哪些 observable,就会和哪些状态建立依赖。项目小的时候这很方便,项目大了以后,一次状态更新到底影响了多少组件,往往不够直观。
当状态量很大、对象嵌套很深、computed 链路复杂时,MobX 的响应式追踪成本也会变得明显。它不是不能优化,而是需要团队对响应式边界有很强的控制能力。
适合 MobX 的场景:
- 业务更接近对象建模。
- 状态之间有比较多派生关系。
- 团队熟悉响应式模型。
- 项目规模中等,状态边界清晰。
不适合的场景是:全局状态持续膨胀、接口数据大量进入 observable、页面经常出现不明原因的大范围更新。
Zustand 适合多数复杂 React 项目的客户端状态
Zustand 的优势是简单、直接、可控。它不像 Redux 那样强调 action/reducer 流程,也不像 MobX 那样依赖自动响应式追踪。它本质上是一个普通 store,加上 React hook 订阅能力。
export const useViewStore = create(set => ({ sidebarCollapsed: false, toggleSidebar: () => set(state => ({ sidebarCollapsed: !state.sidebarCollapsed }))}));
组件通过 selector 订阅自己需要的状态:
const collapsed = useViewStore(state => state.sidebarCollapsed);
这个模型的价值在大型项目里很明显:更新边界更好判断。组件订阅了什么,状态从哪里更新,通常都能在代码里直接追到。
Zustand 的不足是它不提供强约束。store 怎么拆、action 怎么组织、selector 怎么复用、是否允许组件直接组合多个 store,都需要团队自己定规范。它也不适合直接接管大量服务端状态,否则会重复造缓存和失效逻辑。
适合 Zustand 的场景:
- 需要轻量管理客户端全局状态。
- 希望显式控制组件订阅范围。
- 项目已经使用 React Query 管理服务端数据。
- 不希望引入 Redux 的完整流程。
- 需要在复杂页面里保持性能边界清晰。
如果要给大多数中大型 React 项目一个默认建议,我会优先考虑 React Query + Zustand:前者处理服务端状态,后者处理客户端交互状态。
Jotai 适合细粒度和原子化状态
Jotai 的模型是 atom。每个 atom 是一个状态单元,组件订阅 atom,多个 atom 可以组合出派生状态。
const countAtom = atom(0);const doubleAtom = atom(get => get(countAtom) * 2);
它的优势是细粒度。相比一个集中式 store,atom 更像分散的状态节点。组件只订阅自己需要的 atom,局部更新天然更容易控制。
Jotai 适合状态天然可以拆成很多小单元的场景,例如编辑器、复杂配置面板、动态表单、可组合的交互模块。它也适合做一些框架底层状态,因为 atom 可以被组合、派生和复用。
它的问题是状态分散以后,整体结构不一定好理解。项目大了以后,atom 的命名、组织、依赖关系需要严格管理,否则会从“灵活”变成“散”。
适合 Jotai 的场景:
- 状态粒度天然很细。
- 不希望维护一个中心化大 store。
- 有大量可组合派生状态。
- 组件之间共享的是小状态单元,而不是完整业务对象。
不适合的场景是:团队更习惯领域 store,希望从业务模块维度集中管理状态。
Valtio 适合喜欢可变对象写法的轻量响应式场景
Valtio 使用 Proxy 包装状态对象,开发者可以直接修改对象,然后在 React 组件中通过 snapshot 读取。
const state = proxy({ count: 0});state.count += 1;
它的体验接近“直接修改对象,界面响应更新”,比 MobX 更轻,也更少概念。对于一些中小型交互、内部工具或原型项目,Valtio 会显得非常顺手。
但 Proxy 模型也有需要注意的地方。可变写法虽然简单,但在大型团队里可能让状态修改入口变得分散。snapshot 和 proxy 的使用边界也需要理解,否则容易写出行为不清晰的代码。
适合 Valtio 的场景:
- 希望用可变对象方式组织状态。
- 项目规模不大,状态入口可控。
- 需要轻量响应式体验。
- 团队能接受 Proxy 和 snapshot 的心智模型。
如果项目追求强工程约束,或者需要严格审查所有状态变更入口,Valtio 未必是首选。
Recoil 的思想值得了解,但选型要谨慎
Recoil 提出了 atom 和 selector 的数据流图模型。这个思路对后来很多状态库都有启发:状态可以拆成原子节点,派生状态可以通过 selector 组合出来,组件只订阅自己需要的节点。
从模型上看,Recoil 很适合复杂派生状态和跨组件共享状态。但从工程选型角度看,它现在需要谨慎评估。社区活跃度、维护状态、和 React 新特性的长期适配,都比模型本身更重要。
如果只是学习状态管理思想,Recoil 仍然有参考价值;如果是新的生产项目,通常会更倾向于 Jotai、Zustand 或 Redux Toolkit。
TanStack Query 和 SWR 不应该被当成普通全局 Store
TanStack Query 和 SWR 经常被放进状态管理讨论里,但它们解决的是服务端状态,不是普通客户端状态。
它们关注的问题包括:
- 接口请求何时发起。
- 数据是否新鲜。
- 缓存何时失效。
- 窗口重新聚焦是否刷新。
- 弱网失败是否重试。
- mutation 后如何同步列表和详情。
这些事情如果用普通 store 手写,会产生大量重复逻辑。TanStack Query 和 SWR 的价值就在于把服务端状态从客户端 store 中剥离出来。
但它们不适合管理纯客户端交互状态。比如侧边栏是否收起、弹窗是否打开、当前选中的临时行,这些状态不需要请求缓存能力,用 Zustand、Jotai 或组件局部状态更直接。
不同工具背后的模型差异
把主流方案放在一起看,它们本质上不是同一类东西。
| 工具 | 核心模型 | 优势 | 主要风险 |
|---|---|---|---|
| Redux Toolkit | 单向数据流、slice、reducer | 规范强、调试好、团队协作稳定 | 对简单状态偏重 |
| MobX | observable 响应式对象 | 表达自然、computed 强 | 隐式依赖和大状态量性能风险 |
| Zustand | store + selector | 轻量、显式订阅、性能边界清晰 | 需要团队自定规范 |
| Jotai | atom 原子状态 | 细粒度、组合灵活 | atom 分散后治理成本上升 |
| Valtio | Proxy 可变对象 | 写法自然、概念少 | 修改入口容易分散 |
| Recoil | atom + selector 数据流图 | 思想完整、派生状态清晰 | 新项目选型需要谨慎 |
| TanStack Query | 服务端状态缓存 | 缓存、失效、同步能力强 | 不替代客户端状态 |
| SWR | 数据请求与重新验证 | 轻量、适合请求缓存 | 复杂业务同步能力较弱 |
大型 To B 项目里的推荐选择
大型 To B 项目的状态管理和普通 C 端页面不太一样。它的页面往往更重,数据关系更复杂,交互链路也更长。
典型场景包括:
项目列表和项目详情互相联动权限、角色、成员信息影响页面可见性大表格存在筛选、排序、分页、选中行和批量操作动态表单存在条件可见、条件必填、条件默认值和字段联动页面同时存在弹窗、抽屉、详情面板和局部编辑态多个模块共享同一份服务端数据
这些场景最怕两类问题:一类是数据不一致,另一类是性能失控。
数据不一致通常来自服务端状态被复制到多个前端 store。列表里是一份数据,详情里是一份数据,弹窗里又复制一份数据,某个 mutation 执行后只刷新了其中一部分,用户就会看到不同区域展示不同结果。
性能失控通常来自订阅范围过大。一个字段变化带动整张表格刷新,一个筛选条件变化导致所有表单项重新计算,一个权限状态更新触发整个页面树重渲染。To B 页面组件多、状态多、表格和表单都重,这种问题会非常明显。
所以在大型 To B 项目里,我会把推荐方案收敛为三层:
| 层级 | 推荐方案 | 原因 |
|---|---|---|
| 服务端状态 | React Query / TanStack Query,或已有 Redux 体系下的 RTK Query | 负责缓存、失效、重试、后台刷新和 mutation 后同步 |
| 客户端全局状态 | Zustand | 用 selector 控制订阅范围,适合页面偏好、当前空间、跨页面选择等状态 |
| 局部复杂状态 | useReducer、局部 store、表单框架内部状态,必要时评估 Jotai | 避免把高频局部变化扩散到全局 |
这里最推荐的是 React Query + Zustand。
React Query 负责服务端数据,让列表、详情、成员、权限这些数据有统一缓存来源。Zustand 只保存纯客户端状态,例如当前工作空间、当前视图模式、侧边栏状态、跨页面临时选择。这样可以避免“接口数据进 store 后再手动同步”的问题。
从性能角度看,这套组合也更稳:
- 服务端数据变化时,通过 query key 精准刷新相关数据。
- 客户端状态变化时,通过 Zustand selector 控制组件订阅范围。
- 大表格的数据源不需要被深层响应式代理包裹。
- 动态表单的字段联动可以留在表单框架内部,不污染全局 store。
- 高频输入、拖拽、展开收起等局部状态不进入全局状态系统。
Redux Toolkit 在大型 To B 项目里不是不能用。它适合组织规模很大、需要强规范、强审计、强调试的团队。如果项目已经有成熟 Redux 体系,继续使用 Redux Toolkit 加 RTK Query 是合理选择。但如果从零开始,并且团队没有强烈的 Redux 历史包袱,Redux Toolkit 对很多页面状态来说会偏重。
Jotai 可以作为特定复杂模块的补充。比如动态表单、可视化编辑器、规则配置器,这类场景中字段、规则、面板状态天然是细粒度节点,atom 模型会比较自然。但不建议在整个大型 To B 项目里无边界地使用 Jotai,否则 atom 分散后会带来治理成本。
大型 To B 项目里不推荐的使用方式
不推荐第一种:用 MobX 承载大量服务端数据和页面状态。
MobX 的问题不是不能做 To B,而是它在“大状态量 + 深层对象 + 高频更新 + 多组件观察”场景下风险比较高。To B 页面里常见的大表格、动态表单、权限树、组织架构树,如果都进入 observable,对象初始化、依赖追踪、computed 重算和组件更新都可能变重。
尤其是动态表单场景,一个字段变化可能影响其他字段的可见性、必填性、有效性和默认值。如果这些计算都叠在 MobX 的响应式图里,排查性能问题会很困难。你看到的是“改了一个字段”,实际可能触发了一大片 observable 依赖和 computed 链路。
不推荐第二种:用 Zustand 保存所有接口返回数据。
Zustand 适合客户端状态,但不是服务端缓存工具。把项目列表、成员列表、详情数据、权限数据都放进 Zustand,短期看很简单,长期会遇到缓存过期、重复请求、刷新时机、mutation 后同步这些问题。最后往往会在 Zustand 上再手写一套 React Query 已经解决的问题。
不推荐第三种:把高频局部状态放进全局 store。
表单输入值、鼠标拖拽位置、正在编辑的单元格、展开中的临时面板,这些状态如果每次变化都写入全局 store,会放大渲染范围。大型 To B 页面本来组件就重,高频状态应该尽量留在局部组件、局部 context 或专门的表单状态系统里。
不推荐第四种:在没有规范的情况下大面积使用原子化状态。
Jotai 的 atom 很灵活,但大型项目里灵活本身也是成本。如果 atom 到处创建,派生关系分散在多个文件,过一段时间就很难判断某个业务状态到底由哪些 atom 组成。它更适合作为复杂模块内部的精细化工具,而不是所有状态的默认容器。
不推荐第五种:用 Recoil 作为新项目的主状态方案。
Recoil 的思想值得学习,但新项目选型要考虑长期维护、社区活跃度和 React 新特性的适配。大型 To B 项目通常生命周期很长,状态库不能只看模型是否漂亮,还要看它未来几年是否稳定可靠。
综合来看,在大型 To B 项目中比较稳妥的选择是:
| 推荐等级 | 方案 | 说明 |
|---|---|---|
| 首选 | React Query + Zustand | 服务端状态和客户端状态边界清晰,性能风险可控 |
| 可选 | Redux Toolkit + RTK Query | 适合强规范团队和已有 Redux 体系 |
| 模块内可选 | Jotai | 适合动态表单、编辑器、配置器等细粒度状态 |
| 谨慎 | MobX | 对象建模舒服,但大状态量下性能和依赖排查风险高 |
| 谨慎 | Valtio | 适合轻量响应式,强工程约束项目要控制修改入口 |
| 不建议新项目主用 | Recoil | 思想有价值,但生产选型需要考虑长期维护风险 |
我会如何做技术选型
如果是一个普通中大型 React 业务系统,我会优先从这个组合开始:
React Query 管服务端状态Zustand 管客户端全局状态useState/useReducer 管局部状态表单库管表单内部状态
这套组合的好处是边界清晰。接口数据不会进入客户端 store,客户端交互状态也不会被请求缓存工具强行接管。
如果团队非常大,且需要强约束、统一调试和长期可维护的数据流,可以选择 Redux Toolkit。尤其是已有 Redux 体系的项目,不一定有必要为了“轻量”而迁移。
如果业务更像复杂对象模型,并且团队很熟悉响应式系统,可以使用 MobX。但必须控制 observable 范围,避免把接口大对象和高频变化状态都塞进同一个全局响应式对象图里。
如果是动态表单、编辑器、配置器这类细粒度状态很多的业务,可以评估 Jotai。它的 atom 模型会比中心化 store 更自然。
如果是内部工具、原型项目或交互状态较少的应用,Valtio 可以提供很低成本的响应式体验。
小结
状态管理没有一个永远正确的答案。真正重要的是先识别状态类型,再选择匹配的模型。
服务端状态需要缓存、失效和同步,所以应该优先考虑 TanStack Query、SWR 或 RTK Query。客户端状态需要关注共享范围和更新边界,所以可以在 Zustand、Redux Toolkit、MobX、Jotai、Valtio 之间选择。
对复杂 React 项目来说,最危险的不是选错某个库,而是用一个库管理所有状态。接口数据、页面交互、表单草稿、视图偏好、派生计算,如果都进入同一个全局状态系统,后面一定会变得难以维护。
更合理的方向是让每类状态回到适合它的位置。状态管理工具只是手段,清晰的状态边界才是长期可维护的基础。