授权与合规声明
本文全部操作对象均为自建隔离环境与已获授权的目标,涉及安全测试或以他人系统为对象的环节,必须以取得合法授权为前提。未经授权的测试违反《中华人民共和国网络安全法》与《刑法》相关条款,须承担相应法律责任。本文只讲技术工程实践,不含任何攻击步骤、利用载荷与绕过手法,请勿将文中任何内容用于非自有、未获授权的系统。
第1章 为什么状态管理是前端基建
1.1 全局/共享状态的必要性
现代前端应用早已不是"一个页面一个状态"。登录用户信息、主题切换、购物车、跨页面的表单草稿,这些状态需要在组件树的不同分支之间共享。如果只用 props 一层层往下传,或者靠 Context 硬塞,组件耦合会迅速膨胀,维护成本随之上升。状态管理库的价值,就是把"共享状态"从组件内部抽离出来,变成一处可读写、多处可订阅的独立层。它不是银弹,但在中大型应用里几乎是绕不开的基建。
也要记住另一面:不是所有状态都该进全局库。纯组件内部、不会跨组件共享的临时值,用 useState 就够了;强行提拔到全局,反而增加不必要的复杂度。判断标准只有一条——这个状态是否真的被多个互不相关的组件共享,或需要在组件卸载后仍保留。
1.2 服务端状态 vs 客户端状态
选库之前,先分清两类状态。服务端状态指来自接口、带有缓存与失效语义的数据,比如文章列表、用户资料。这类状态更适合用 React Query / TanStack Query 这类数据缓存库管理,它们内置去重、后台刷新、失效重取等能力。客户端状态才是 Redux、Zustand、Jotai、Pinia 的用武之地:主题、弹窗开关、本地草稿、表单临时值。把服务端状态塞进全局 store 是常见误用,后面第6章会展开。
// 用 TanStack Query 管理服务端状态,而非塞进全局 storeimport{useQuery}from'@tanstack/react-query';functionuseArticleList(){returnuseQuery({queryKey:['articles'],queryFn:()=>fetch('/api/articles').then((res)=>res.json()),});}第2章 四个库各自的定位
2.1 Redux(含 Redux Toolkit)
Redux 的核心是"单一数据源 + 纯函数 reducer + 可预测的状态变更"。它的强项在于可预测性与成熟的中间件生态:时间旅行调试、action 日志、副作用编排(redux-thunk / redux-saga)在大型团队里非常有价值。截至 2026-10-25,Redux 官方明确把 Redux Toolkit(@reduxjs/toolkit)作为编写 Redux 逻辑的标准方式,它用 createSlice 消除手写 action type 与不可变更新的样板。新项目若选 Redux,应当直接采用 Toolkit,而不是裸写 redux 核心包。
Redux 真正的护城河是"可审计"。当业务里存在大量异步编排、需要回放每一次状态变更、要在团队间建立统一约定时,它的中间件与 devtools 组合很难被轻量库替代。代价是心智负担与样板相对更高,小项目用它会显得笨重。
2.2 Zustand
Zustand 由 pmndrs 团队出品,理念是"用最少的样板提供全局状态"。它把 store 暴露成自定义 hook,无需 Provider 包裹,组件中直接调用即可。对中短期、规模中等的 React 应用,它几乎是最省心的默认选择:上手快、TypeScript 支持好、生态中间件(persist、devtools、immer)开箱即用。它的代价是结构化约束偏弱,超大型团队若缺乏约定,store 容易组织混乱。因此用 Zustand 时,建议团队自己定一份"按功能切片、store 内聚"的约定,别让全局状态变成随意堆放的抽屉。
2.3 Jotai
Jotai 同样出自 pmndrs,走的是"原子化"路线。状态的基本单位是一个个 atom,组件订阅某个 atom 时,只有真正用到它的组件才重渲染。对于大量互相独立、又彼此派生的小状态(比如复杂表单的几十个字段、筛选组合、派生视图),Jotai 的细粒度响应优势明显。代价是 atom 图在大型应用里可能散落难追踪,调试需要换一种心智模型。Jotai 适合"状态天生就碎"的场景,不适合把本来聚合良好的领域状态硬拆成几十个原子。
// Jotai:以 atom 为基本单位,按需派生 import { atom } from 'jotai'; const userNameAtom = atom(''); const statusAtom = atom('idle'); // 派生 atom:基于已有 atom 计算,不额外触发重渲染 const greetingAtom = atom((get) => { const name = get(userNameAtom); return name ? `你好,${name}` : '请登录'; });2.4 Pinia
Pinia 是 Vue 官方状态库,也是 Vuex 的继任者。截至 2026-10-25,在 Vue 3 项目中它被官方推荐为默认状态方案。API 贴近组件写法,类型可自动推断,支持 devtools、模块化拆分与热更新。只要是 Vue 技术栈,几乎不必纠结——选 Pinia 即可。它不属于 React 生态,选型时第一步就是先定框架。对 Vue 团队来说,Pinia 几乎不存在"要不要选"的问题,更多是"怎么组织 store 文件"的工程问题。
// Pinia:defineStore 定义一个 storeimport{defineStore}from'pinia';exportconstuseCounterStore=defineStore('counter',{state:()=>({counter:0,status:'idle'}),getters:{doubleCounter:(state)=>state.counter*2,},actions:{increment(amount:number){this.counter+=amount;},},});第3章 对比维度
3.1 包体积 / 样板代码 / TS 支持 / 调试体验 / 学习曲线
下面这张表从五个常被拿来比较的维度做横评。注意:包体积数字会随版本变化,下表标"待验证",以各库官方发布为准;维度评分是相对主观的归类,不是定量结论。
| 维度 | Redux Toolkit | Zustand | Jotai | Pinia |
|---|---|---|---|---|
| 包体积(gzip,待验证) | 较大 | 极小 | 很小 | 很小 |
| 样板代码量 | 中等 | 极少 | 少 | 少 |
| TypeScript 支持 | 优秀 | 优秀 | 优秀 | 优秀 |
| 调试体验 | 强(时间旅行) | 一般(需中间件) | 一般 | 强(Vue devtools) |
| 学习曲线 | 较陡 | 平缓 | 平缓但需换心智 | 平缓 |
第4章 选型决策表
4.1 按框架+规模+复杂度
选型判据不是"谁更好",而是"框架 + 项目规模 + 状态复杂度"。下面给出场景到推荐的映射。注意最后一列是"为什么",帮你在边界场景里自己推演,而不是死记结论。
| 场景 | 推荐 | 理由 |
|---|---|---|
| Vue 技术栈项目 | Pinia | 官方默认,零纠结 |
| React 中大型团队、复杂状态流、强调试需求 | Redux Toolkit | 可审计、生态成熟 |
| React 中小型项目、追求轻量省心 | Zustand | 样板少、上手快 |
| React 大量独立/派生小状态、细粒度重渲染 | Jotai | 原子化、按需响应 |
| 服务端数据(接口缓存) | TanStack Query | 缓存失效语义内建 |
| 纯 UI 局部状态 | useState + Context | 无需引入依赖 |
| — | — | |
| Vue 技术栈项目 | Pinia | |
| React 中大型团队、复杂状态流、强调试需求 | Redux Toolkit | |
| React 中小型项目、追求轻量省心 | Zustand | |
| React 大量独立/派生小状态、细粒度重渲染 | Jotai | |
| 服务端数据(接口缓存) | TanStack Query | |
| 纯 UI 局部状态 | useState + Context |
第5章 代码对比
5.1 Redux Toolkit 一个 slice
下面的示例用 createSlice 定义一个计数器 slice。⚠️ 代码待验证,请以官方文档为准。
import{createSlice,PayloadAction}from'@reduxjs/toolkit';interfaceCounterState{counter:number;status:string;}constinitialState:CounterState={counter:0,status:'idle'};constcounterSlice=createSlice({name:'counter',initialState,reducers:{incrementByAmount(state,action:PayloadAction<number>){state.counter+=action.payload;state.status='changed';},reset(state){state.counter=0;state.status='idle';},},});exportconst{incrementByAmount,reset}=counterSlice.actions;exportdefaultcounterSlice.reducer;5.2 Zustand 一个 store
下面用 create 定义一个同等语义的 store。⚠️ 代码待验证,请以官方文档为准。
import{create}from'zustand';interfaceCounterStore{counter:number;status:string;incrementByAmount:(amount:number)=>void;reset:()=>void;}exportconstuseCounterStore=create<CounterStore>((set)=>({counter:0,status:'idle',incrementByAmount:(amount)=>set((state)=>({counter:state.counter+amount,status:'changed',})),reset:()=>set({counter:0,status:'idle'}),}));状态管理选型速查卡:把第3、4章的对比与决策整理成一张卡。放在资料包里,扫码即可获取:
第6章 常见误用
6.1 把服务端状态塞进全局 store
接口数据自带缓存、失效、并发语义,用全局 store 手动维护容易写出重复请求与陈旧数据。交给 TanStack Query 这类库,比自己造轮子稳得多。一个简单判据:这份数据"来自网络、会过期、会被多个地方读取"——它八成是服务端状态。
6.2 过度拆分原子(Jotai)
atom 拆得太碎,派生关系网会难以追踪,调试时找不到状态来源。按"业务边界"而非"每个字段一个 atom"来组织,必要时用派生 atom 聚合。
6.3 在 Redux 里写副作用不中规中矩
把异步、副作用直接写进 reducer 会破坏可预测性。应走 redux-thunk / createAsyncThunk,或把副作用放到中间件,保持 reducer 纯函数。一旦 reducer 里出现 await 或随机数,调试器和时间旅行就失去意义,问题会很难复现。
| 误用 | 表现 | 建议 |
|---|---|---|
| 服务端状态进 store | 重复请求、数据陈旧 | 用 TanStack Query |
| Jotai 原子过碎 | 调试困难、关系网混乱 | 按业务边界聚合 |
| Redux 副作用乱写 | 状态不可预测 | 走 thunk / 中间件 |
第7章 迁移与渐进策略
7.1 不要一次性重写
状态层牵一发动全身。把整库替换当成一次大重写,风险与回归成本都高。更稳妥的是先在小模块试点新方案,验证心智模型与团队接受度。观察两周:开发者是否顺畅、渲染性能是否达标、调试是否还方便——再决定是否推广。
7.2 新老并存平滑过渡
现代库大多能并存:Zustand 与 Jotai 可通过桥接库协同,Redux 也能逐步把 reducer 迁移到 createSlice。关键是在边界处明确"哪块状态归谁",避免两套状态互相抢源。迁移过程中保留清晰的接口层,让组件不直接依赖某一套实现,后续再换就更从容。
前端选型决策树:把第4章的决策表扩展成一张流程图。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| 事实 | 出处 | 本文位置 |
|---|---|---|
| Redux Toolkit 是 Redux 官方推荐的写法 | Redux 官方文档 redux.js.org / redux-toolkit.js.org | 第2章 2.1 |
| Pinia 是 Vue 官方状态库,继任 Vuex | Pinia 官方文档 pinia.vuejs.org 与 Vue 官方资料 | 第2章 2.4 |
| Zustand 由 pmndrs 团队维护 | GitHub pmndrs/zustand | 第2章 2.2 |
| Jotai 由 pmndrs 团队维护 | GitHub pmndrs/jotai | 第2章 2.3 |
| 各库包体积 gzip 数值 | 待验证,以各库官方发布为准 | 第3章 3.1 |
| 具体版本号 | 待验证 | 全文 |
附表 B:术语速查表
| 术语 | 含义 |
|---|---|
| 单一数据源 | Redux 的核心原则,整个应用状态存于一棵 state 树 |
| reducer | 接收旧状态与 action、返回新状态的纯函数 |
| atom | Jotai 中状态的最小单位,可独立订阅与派生 |
| slice | Redux Toolkit 中按功能划分的一块状态与逻辑 |
| store | 状态容器,提供读写与订阅能力 |
| 服务端状态 | 来自接口、带缓存与失效语义的数据 |
写在最后:这篇用到的资料
写这篇文章时,整理了前端工程化的选型方法,顺手也整理了几份配套的东西:
- 大模型学习路线:从基础到落地的整套路径图
- Claude Skills 全指南:知识库搭建与技能编排实战
- LangGraph+MCP 智能体实战视频:从 0 到 1 跑通一个智能体
资料是我自己整理的,放在下面这个码上,扫码即可获取:
资料较多,建议先看「全套 AGI 大模型学习路线」,再挑一个实战项目跟练。