3.3 状态管理选型


文档摘要

3.3 状态管理选型 本节摘要:React 的状态管理不是「选个库」,而是「按复杂度定方案」。本节先用同一个计数器分别实现 Redux、Zustand、Recoil 三个版本,让你在代码对比中看清各家的思路;再给出「组件内 state → Context → Zustand → Redux Toolkit」的升级路径,建立按规模选型的判断框架。 阅读收获 阅读完本节,你应当能够: 说出 Redux 的三个核心原则与四个核心概念 用 Redux Toolkit 的 configureStore、createSlice 搭建状态 用 Zustand 的 create 定义 store 并消费 理解 Recoil 的 atom 与 selector 模型 依据应用复杂度在四套方案之间做出选型

3.3 状态管理选型

本节摘要:React 的状态管理不是「选个库」,而是「按复杂度定方案」。本节先用同一个计数器分别实现 Redux、Zustand、Recoil 三个版本,让你在代码对比中看清各家的思路;再给出「组件内 state → Context → Zustand → Redux Toolkit」的升级路径,建立按规模选型的判断框架。

阅读收获

阅读完本节,你应当能够:

  1. 说出 Redux 的三个核心原则与四个核心概念
  2. 用 Redux Toolkit 的 configureStore、createSlice 搭建状态
  3. 用 Zustand 的 create 定义 store 并消费
  4. 理解 Recoil 的 atom 与 selector 模型
  5. 依据应用复杂度在四套方案之间做出选型

一、问题与直觉:状态多到管不住时怎么办

Context 解决「跨层传数据」,但真实应用的状态问题不只是「传」——还有「谁在改、改完什么效果、能不能回退」。多人协作的大型应用里,状态改得乱七八糟是最常见的崩坏方式:A 模块改了 B 模块的数据,B 模块又反改 A 的,互相踩脚。

状态管理库的核心价值不是「存数据」,而是把「改状态」这件事变得可预测。Redux 用单向数据流 + 纯函数约束更新;Zustand 用极简 API 降低心智负担;Recoil 用原子粒度追求精确订阅。它们解决同一个问题的思路不同,代价也不同。

二、Redux:可预测但样板多

Redux 是一个可预测的状态容器,遵循三个核心原则:单一数据源(整个应用只有一个 store)、状态只读(不能直接改,只能 dispatch action)、纯函数修改(reducer 是纯函数)。

核心概念

  • Store:保存整个应用的状态,单一数据源
  • Action:描述「发生了什么」的普通对象
  • Reducer:纯函数,接收旧状态和 action,返回新状态
  • Dispatch:派发 action,通知 store 需要更新
  • Selector:从 store 中提取数据的函数

把五者串成一个故事:界面发生了一件事(比如用户点击加一),组件把这件事描述成一个 action(「计数加一」),dispatch 把这个 action 丢给 reducer,reducer 按规则算出新状态,store 存下新状态并通知所有订阅的组件。界面看到新状态,重新渲染。整个流程单向、可追踪,任何一步出了错,都能顺着链路查回去——这就是「可预测」的含义。

数据流是单向循环:

手写 Redux 计数器

经典写法分四步:actions、reducer、store、组件。

// actions export const INCREMENT = 'INCREMENT'; export const increment = () => ({ type: INCREMENT }); // reducer const initialState = { count: 0 }; const counterReducer = (state = initialState, action) => { switch (action.type) { case INCREMENT: return { ...state, count: state.count + 1 }; default: return state; } }; // store const store = createStore(counterReducer); // 组件 function Counter() { const count = useSelector(state => state.count); const dispatch = useDispatch(); return ( <div> <p>计数:{count}</p> <button onClick={() => dispatch(increment())}>加一</button> </div> ); }

这里能看到 Redux 的代价:一个计数器要写 actions、reducer、store、组件四处。样板代码多、学习曲线陡,是 Redux 被诟病的两个点。

Redux Toolkit:官方推荐的现代化写法

Redux Toolkit 用 configureStore 和 createSlice 大幅压缩样板。createSlice 把 action 和 reducer 合并定义:

// counterSlice import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { count: 0 }, reducers: { increment: (state) => { state.count += 1; }, decrement: (state) => { state.count -= 1; }, }, }); export const { increment, decrement } = counterSlice.actions; export default counterSlice.reducer; // store import { configureStore } from '@reduxjs/toolkit'; const store = configureStore({ reducer: { counter: counterReducer }, });

createSlice 内部用 Immer 支持「看起来像直接修改」的写法(state.count += 1),实际仍是不可变更新。组件消费方式和经典版一致。新项目用 Redux 一律用 Toolkit,不要手写样板。

三、Zustand:小、快、无样板

Zustand 是另一个极端——极简。一个 create 定义整个 store,状态和修改函数放一起,无 Provider、无样板。

import { create } from 'zustand'; const useStore = create((set) => ({ count: 0, increment: () => set(state => ({ count: state.count + 1 })), decrement: () => set(state => ({ count: state.count - 1 })), })); // 组件 function Counter() { const { count, increment } = useStore(); return <button onClick={increment}>计数:{count}</button>; }

Zustand 不需要 Provider 包裹,store 是模块级对象,组件直接 import 使用。它支持选择器订阅——useStore(state => state.count) 只订阅 count,别的字段变了不重渲染,这是它性能好的关键。

Zustand 的选择器与 get 用法

Zustand 的 create 回调里可以拿到 set 和 get 两个函数:set 用于更新,get 用于读取当前状态。需要「基于最新状态做判断」的场景用 get:

import { create } from 'zustand'; const useCart = create((set, get) => ({ items: [], total: 0, addItem: (item) => set(state => ({ items: [...state.items, item], total: state.total + item.price, })), checkout: () => { // get 读取最新状态 const { items, total } = get(); if (items.length === 0) return alert('购物车为空'); // 提交订单... alert(`共 ${items.length} 件,合计 ${total} 元`); }, }));

get 的价值在于「先读后写」的复合操作。它不受闭包快照影响——每次都拿当前最新状态,比在 set 回调外直接引用变量安全。

组件外使用 Zustand

Zustand 一个经常被低估的能力:store 可以在 React 组件之外使用。请求逻辑、工具函数、事件监听器都能直接调 store 的 action,不依赖组件上下文。这比 Context(只能在组件内读)灵活得多:

// 在工具函数里更新状态 import useCart from './cartStore'; export function handleApiSuccess(data) { useCart.getState().setItems(data.items); }

「组件外也能改状态」是把双刃剑——方便的同时,状态更新的来源更难追踪。Zustand 适合这种用法,Redux 就不适合(它的强约束就是要在单一数据流里改)。这也是选型时的一个隐形考量。

维度 Redux Zustand
样板代码 多(Toolkit 后减少) 极少
学习曲线
性能 需 selectors 优化 默认按需订阅
调试工具 Redux DevTools React DevTools
组件外使用 可以但绕 直接 getState 方便
适用 大型复杂应用 中小型项目

Redux Toolkit 的异步处理

真实的 Redux 应用必然有异步操作——请求数据。Redux Toolkit 提供了 createAsyncThunk 处理「pending / fulfilled / rejected」三态:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; const fetchUser = createAsyncThunk('user/fetch', async (userId) => { const res = await fetch(`/api/user/${userId}`); return res.json(); }); const userSlice = createSlice({ name: 'user', initialState: { data: null, status: 'idle', error: null }, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.status = 'loading'; }) .addCase(fetchUser.fulfilled, (state, action) => { state.status = 'done'; state.data = action.payload; }) .addCase(fetchUser.rejected, (state, action) => { state.status = 'error'; state.error = action.error.message; }); }, });

组件里 dispatch(fetchUser(123)) 触发整个流程。loading、error、data 三态由 slice 统一管理,组件只管展示。这是现代 Redux 异步的标准姿势——它替代了早期手写 action 三态的繁琐做法。

不可变更新的底层:为什么不能直接改 state

Redux 要求 reducer 返回新状态而非修改旧状态,核心原因有两个:

第一,可预测性。reducer 是纯函数——同样的输入必然同样的输出。直接修改旧状态会留下「改了哪、没改哪」的隐式状态,破坏纯函数性质,也让时间旅行调试失效。

第二,性能与正确性。React 靠「引用是否变化」判断状态是否更新。直接改 state 对象内部,引用没变,React 不重渲染,界面和状态就脱节了。展开运算符 { ...state, count: state.count + 1 } 保证了新对象、新引用。

手写展开容易出错(嵌套层级一深就头大),Redux Toolkit 内置的 Immer 在 createSlice 里自动处理——你写 state.count += 1 这种「看似直接改」的代码,Immer 底层帮你生成新对象。用了 Toolkit,就不用手写不可变展开;手写 Redux,必须记得展开。

四、Recoil:原子化状态

Recoil 是 Facebook 出的状态库,思路是「原子化」——状态由一个个 atom(原子)组成,selector 从 atom 派生计算状态。

import { atom, selector, useRecoilState } from 'recoil'; const countAtom = atom({ key: 'count', default: 0, }); const doubleCount = selector({ key: 'doubleCount', get: ({ get }) => get(countAtom) * 2, }); function Counter() { const [count, setCount] = useRecoilState(countAtom); const double = useRecoilValue(doubleCount); return ( <div> <p>计数:{count},翻倍:{double}</p> <button onClick={() => setCount(count + 1)}>加一</button> </div> ); }

atom 是状态的基本单元,selector 是派生状态——就像计算属性,atom 变了它自动重算。Recoil 的优势是细粒度订阅:不同组件只订阅自己用的 atom,互不干扰。它的使用需要 RecoilRoot 包裹应用。

Recoil 的场景定位比较明确:适合「状态关系复杂、派生状态多」的应用——比如大量计算属性、状态之间有依赖关系。但它的发展节奏慢,社区热度不及 Redux 与 Zustand,新项目选它之前要想清楚「值不值得为原子模型引入一个生态没那么厚的库」。

五、四套方案对比与选型

把四套方案放一张对比矩阵:

状态管理方案对比矩阵

状态管理方案对比矩阵

这张矩阵从四个维度比较:学习成本、样板代码、性能特性、调试能力。Redux 左下角(高成本高约束),Zustand 右下角(低成本高效率),Context 处于最轻一端,Recoil 偏重订阅精确性。选型先看图,再按你的状态复杂度对号入座。

选型不要背口诀,用复杂度做标尺:

状态复杂度 方案 理由
组件内部 useState / useReducer 不需要全局共享
少量全局共享 Context 内建,零依赖
中等复杂、追求效率 Zustand 样板少、性能好
大型复杂、团队协作 Redux Toolkit 可预测、DevTools 强
细粒度派生状态多 Recoil atom/selector 模型

💡 关键直觉:选状态库是「用复杂度换复杂度」。简单状态上重型库,是给自己加不必要的负担;复杂状态上轻量方案,是给团队挖维护的坑。先数数你的状态有多少、变更频率多高、要不要时间旅行调试,再选。

⚠️ 常见坑:把「别人都说 Redux 好」当选型依据。技术选型要为自己的项目负责——团队熟不熟、状态复杂不复杂、要不要 DevTools 的时间旅行调试,这些才是变量。库的热度只能当背景噪音。

六、工程实践:一套实用建议

基于实际项目经验给几条具体建议:

第一,默认从 Context + useReducer 起步。 大多数应用的全局状态没那么复杂,这两件套够用,且零依赖。第 2.5 节已经讲过 Provider + 自定义 Hook 的模板,直接套用。

第二,状态开始跨模块纠缠时,切 Zustand。 判断信号:多个页面要同步同一份数据、更新逻辑开始出现在多处、Context 的 value 越来越像「一个大杂烩对象」。Zustand 迁移成本低——store 定义独立、消费用 Hook,改动局部。

第三,只有团队规模、状态复杂度、可预测性要求同时高时才上 Redux Toolkit。 Redux 的价值在于强约束(单向数据流、纯 reducer)和 DevTools(时间旅行、action 日志),这些在大型协作项目里能救命的特性,在小型项目里是纯负担。

第四,别同时上多个状态库。 一个项目一种全局状态方案,多了心智负担爆炸。迁移是重构,不是堆叠。

状态设计的原则

选完库,还要会「设计状态」。三个实用原则:

原则一:状态分三层。 组件局部状态(useState)、共享状态(Context/Zustand)、服务器状态(从接口来的数据)。服务器状态值得单独对待——它有自己的 loading、error、缓存、过期逻辑,现在常用 TanStack Query 这类数据请求库管理,而不是塞进全局 store。把接口数据和 UI 状态混在一个 store 里,是大型项目最常见的状态混乱源头。

原则二:状态尽量小。 能本地就不共享,能派生就不存储。第 2.4 节讲的「派生状态勿存」在这里同样成立——能从现有数据算出来的(比如「剩余金额 = 总额 - 已用」),别单独存一份。

原则三:变更函数集中。 无论是 Context 的 Provider 还是 Zustand 的 store,把「怎么改状态」的函数集中在定义处。组件只调用,不实现——这样改状态规则时只动一处,组件层零改动。

一个选型决策的实战推演

假设你在评估一个中型后台管理系统:登录态、用户信息、侧边栏菜单、几十个表单页。推演一遍:

登录态和用户信息是「全局低频」——Context 够用。菜单状态也是低频——Context 或组件内部都行。几十个表单页各自的状态是「组件局部」——useState 就够,不需要进全局 store。服务器数据(列表、详情)是「服务器状态」——应该考虑数据请求库,而非塞进状态库。

推演下来,这个系统可能根本不需要 Zustand 或 Redux。全局状态就那几样低频数据,Context + useState 全覆盖。真正需要 Redux 的场景,是状态之间有复杂依赖、多个模块并发修改同一份数据、需要回退到任意历史状态——这些特征在你项目里没有时,就别为了「专业」而引入。

常见选型疑问快答

「Redux 的时间旅行调试到底有什么用?」 它不是花架子。时间旅行调试意味着你可以回放到任意一次 action 前后的状态,逐帧查看「这个状态是什么时候变错的」。排查「某份数据被谁改坏了」这类问题,DevTools 里看 action 日志比打日志高效得多。Zustand 的 devtools 中间件也能接入 Redux DevTools,但数据格式和展示细节不如 Redux 原生方案完整。这是「可预测性」在实际排查里的价值——没有它,你得靠猜。

「Context 和 Zustand 能混用吗?」 能,但要有明确边界。常见做法是「低频全局数据走 Context,中高频走 Zustand」——主题、语言这种几乎不变的数据用 Context 就够,购物车、实时通知这种频繁变化的状态进 Zustand。混用的关键是别把同一个状态在两个地方各存一份,那会制造「两处不同步」的新问题。第 2.5 节讲过 Context 的广播式订阅,正因为它「值变全体重渲染」,才不适合高频数据——这正好和 Zustand 的按需订阅互补。

「团队迁移到 Redux Toolkit,旧的手写 action 代码怎么办?」 不急着一次重写。Toolkit 和手写 Redux 的 store 可以共存——老模块继续用 createStore 的手写写法,新模块用 configureStore + createSlice,两者通过 combineReducers 合并进同一个 store。迁移的合理顺序是「新代码全用 Toolkit,老代码遇到改动时顺手迁移」。别为了「统一」搞一次性大重写,那会把迁移风险放大到整个项目。

重点提炼

  • Redux 三原则:单一数据源、状态只读、纯函数修改
  • Redux 四概念:store、action、reducer、dispatch
  • Redux Toolkit:createSlice 压缩样板,createAsyncThunk 管异步,新项目必用
  • 不可变更新:新引用才能触发 React 重渲染,Toolkit 内置 Immer 自动处理
  • Zustand:极简 API、无 Provider、按需订阅,支持组件外 getState 使用
  • Recoil:atom + selector 原子化模型,细粒度订阅,生态偏小
  • 升级路径:useState → Context → Zustand → Redux Toolkit
  • 状态三层:局部状态、共享状态、服务器状态,别混在一个 store
  • 选型标尺:用复杂度换复杂度,状态复杂度和团队变量决定方案
  • 默认起步:Context + useReducer 够用就不引库
  • 实战推演:先数状态复杂度,再决定方案,别为「专业」而引入重型库

下一节处理表单与数据——状态管理的用户侧入口。受控组件、表单验证、数据提交,这些日常占比最高的代码,藏着最多细节坑。表单验证的正则、提交时的防抖与 loading、提交失败的错误回显,每一项都是「看起来简单、做起来讲究」的典型。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U