MODO®
ISSUE 07 / 2026
05 前端思考 / Thinking

状态管理
没有银弹

每个团队都会在某个时刻争论「该用哪个状态库」。这篇文章不站队,而是把 Zustand、Jotai、Redux Toolkit、Recoil 背后的哲学拆开看,再用一份按项目规模的对照实验做背书。

作者 林知远
日期 2026.08.28
时长 11 分钟
标签 状态管理 · React

我们做过一次内部统计:三年里换过四次状态库。从 Redux 到 Dva,到 MobX,再到 Zustand。每次迁移都信誓旦旦说「这次对了」,每次都在两年后推倒重来。

问题不在库本身,而在于没有先回答「我们在管的是什么状态」。这篇文章试图把这个问题彻底讲清楚,再回头看四个主流方案各自适合什么。

01

先厘清:你在管什么状态

「状态」是个被滥用的词。它至少指代三类完全不同的东西,而把它们塞进同一个 store 是大多数混乱的源头。

三类状态的边界

类型 来源 生命周期 该用谁管
服务端状态 API 返回的数据 与远端同步,可失效 React Query / SWR
URL 状态 路由、查询参数 随导航变化 路由 / searchParams
客户端状态 UI 交互、表单、本地草稿 仅当前会话 才是状态库的战场

把服务端数据塞进 Redux 是十年前的标配,今天来看就是把缓存当状态管——你会被迫手写 loading、error、staleTime、缓存键,重新发明 React Query。把这类需求剥离出去后,真正需要状态库处理的,往往比你以为的少得多。

NOTE

状态库的真正职责只剩两件:跨组件共享客户端数据,以及协调多组件的派生计算。如果你只是想在父子间传一个值,useState + Context 足够。

在引入状态库之前,先问一句:这是状态,还是缓存?

— 决定迁移前的自检
02

四个方案背后的哲学

不写「哪个好」,只看它们各自把哪件事当第一性原则。

Zustand:极简的全局 store

import { create } from 'zustand';

const useCart = create((set, get) => ({
  items: [],
  add(item) {
    set((s) => ({ items: [...s.items, item] }));
  },
  get total() {
    return get().items.reduce((sum, i) => sum + i.price, 0);
  },
}));

Zustand 的哲学是「全局单一 store,但用 hook 订阅」。它不强制 action/reducer,更新即函数调用。优势是心智负担最小、API 表面积只有 4 个。代价是——当 store 变大后,切片订阅全靠开发者自觉,没人逼你。

Jotai:原子化的最小单位

import { atom, useAtom } from 'jotai';

// 原子:最小可订阅单元
const priceAtom = atom(0);
const qtyAtom = atom(1);
// 派生原子:依赖变化时自动重算
const totalAtom = atom((get) => get(priceAtom) * get(qtyAtom));

function CartTotal() {
  const [total] = useAtom(totalAtom);
  return <span>¥{total}</span>;
}

Jotai 把状态拆到「原子」粒度,订阅天然精细——只有真正用到的原子变化才会触发渲染。哲学是「自下而上」:不预设全局结构,需要什么就建什么原子。代价是状态间的关系隐式化,调试时要在脑中重建依赖图。

Redux Toolkit:约束优先的可预测性

import { createSlice, configureStore } from '@reduxjs/toolkit';

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] },
  reducers: {
    add(state, { payload }) {
      state.items.push(payload); // Immer 代理,写起来像突变
    },
  },
});

const store = configureStore({
  reducer: { cart: cartSlice.reducer },
  // 内置 devtools、thunk、序列化检测
});

RTK 的哲学是「用结构换可预测」。action/reducer/selector 三段式强制状态变更走单一通道,时间旅行调试、序列化、可测试性都因此受益。代价是样板代码即使被 Immer 和 createSlice 削弱,仍比前两者重。它的甜点是多人协作的中大型项目,约束本身就是生产力。

Recoil:与 React 调度深度耦合

Recoil 的设计目标之一是「并发模式下不阻塞渲染」。它把原子挂载到 React 树上,由 React 自己调度更新。理论上能拿到最接近 React 心智模型的体验,缺点是更新节奏慢、社区活跃度低于前三者,长线投入需要评估。

方案 第一原则 擅长 代价
Zustand 极简全局 store 中小项目、快速起步 切片自觉,store 膨胀后难管
Jotai 原子级订阅 派生多、更新精细 依赖关系隐式,调试需重建图
Redux Toolkit 约束即生产力 多人协作、中大型 样板代码偏重
Recoil 与 React 调度同构 并发模式敏感场景 社区活跃度、维护节奏
03

按规模做对照

光看哲学不够,得拿真实体量说话。我们把同一套购物车 + 用户偏好 + 主题切换的需求,用四个方案各实现一遍,记录关键指标。

1.2KB
Zustand 运行时
3.4KB
Jotai 运行时
11KB
RTK 运行时

小项目里这个差距几乎无感,但当状态规模翻到几十个 slice 时,重渲染次数才是真正的成本。我们用 60 个组件订阅同一 store 的压测结果:

方案 单次更新触发重渲染 需手写切片 派生计算成本
Zustand 视 selector 质量 是 get() 调用时算
Jotai 仅相关原子 否(天然) 依赖图缓存
RTK 视 selector 记忆 是 reselect 记忆

结论很直接:Jotai 在「更新精细化」这件事上结构性胜出,因为它从设计上就按原子订阅;Zustand 和 RTK 靠 selector 记忆,写得好才追平。但 Jotai 的代价是——当原子数量上百后,没人能一眼看懂它们之间的关系。

04

组合而非站队

真实项目里我们从不只用一个方案。最舒服的组合是——分层用不同的工具。

// 分层状态架构:每层用最合适的工具
// 1. 服务端数据 → React Query
const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
});

// 2. URL 状态 → 路由
const [tab, setTab] = useSearchParams();

// 3. 客户端 UI 状态 → Zustand(小型)或 Jotai(派生多)
const useUI = create((set) => ({
  drawerOpen: false,
  toggle: () => set((s) => ({ drawerOpen: !s.drawerOpen })),
}));

// 4. 复杂业务流程 → RTK(如果团队已惯用 reducer 心智)
const orderSlice = createSlice({ /* ... */ });

关键是让每层只做自己擅长的事:React Query 管缓存与失效,路由管可分享的状态,状态库只管真正的客户端交互。混在一起的代价是——你会花大量时间在「这段逻辑到底归谁」上争吵。

TIP

定一条规则:禁止把服务端数据复制进状态库。如果它已经由 React Query 管着,状态库里就只存它的本地视图(如「当前选中的那条」),不要复制整份数据。这一条能消掉一半的状态混乱。

05

决策清单

把前面的讨论收成一份可勾选的清单。下次团队争论时,先把它过一遍:

没有一条是「X 比 Y 好」。所有取舍都锚定在具体的规模、协作密度、派生复杂度上。脱离这些维度谈优劣,都是抽象的空谈。

银弹不存在。但「在正确维度上做选择」是可以学会的。

— 写给争论中的团队

最后一句提醒:选库只是开始,状态架构腐化的速度永远快过你的预期。定期做一次「这个 slice 还有谁在订阅」的清理,比换库更能延长项目寿命。

状态管理 React 架构取舍 实践总结