第101课:Zustand 轻量状态管理——创建 Store、异步 action、中间件、与 Redux 对比
Redux Toolkit 提供了强大且完整的状态管理方案,但它的概念体系(Store、Reducer、Action、Dispatch、Middleware)和样板代码仍然有一定的学习成本。Zustand(德语“状态”)是一个极简、基于 Hook 的轻量状态管理库,它的核心理念是:状态就是一个普通的 JavaScript 对象,修改状态的函数就定义在这个对象旁边。无需 Provider 包裹、无需 action type 常量、无需 dispatch——你直接调用修改函数即可。Zustand 的 API 表面积极小,但足以覆盖绝大多数应用场景,包括异步 action、中间件(如持久化、DevTools)、选择器优化和 TypeScript 泛型。本节课将从零开始,深入 Zustand 的 Store 创建、状态读取与修改、异步操作处理、中间件集成,并最终与 Redux Toolkit 进行公平对比,帮助你在两者之间做出明智的技术选型。
1. Zustand 的核心哲学与安装
Zustand 由 React Spring 和 Jotai 的作者开发,设计目标是在保持 React 函数式编程模型的同时,提供极简的全局状态管理。它的特点包括:
- 无需 Provider:Store 是一个独立于组件树的 JavaScript 模块,直接通过自定义 Hook 访问,不需要在根组件包裹任何 Context Provider。
- 无样板代码:创建 Store 只需一个
create函数,状态和修改函数定义在同一个对象中。 - 基于不可变更新:内部使用
Object.is进行变化检测,与 React 的渲染优化兼容。 - 支持选择器:组件可以选择性地订阅 Store 中的某一部分数据,仅在所选数据变化时重新渲染。
- 支持中间件:内置
persist(持久化)、devtools(Redux DevTools 集成)、immer(Immer 不可变更新)等中间件。
1.1 安装
1 | npm install zustand |
Zustand 的核心包体积极小(约 1KB gzipped),且没有任何外部依赖。如果在 TypeScript 项目中使用,类型定义已内置,无需额外安装 @types。
2. 创建 Store 与基本读写
2.1 create 函数
Zustand 通过 create 函数创建一个 Store。create 接收一个回调函数,该回调接收 set 和 get 作为参数,返回一个包含状态和修改函数的对象。create 的返回值是一个自定义 Hook。
1 | // store.js |
关键解析:
set是一个函数,用于合并更新 Store 中的状态。它接收一个对象(部分状态)或一个回调函数(接收当前state,返回部分状态对象)。与 React 的setState类似,set只更新你传入的字段,其他字段保持不变。get是另一个可选参数,它返回当前的完整状态。在不需要响应式更新、仅想读取最新值的场景中使用get(),例如在异步回调中获取最新状态而不重新渲染组件。create返回的useCounterStore是一个 Hook,可以在任何 React 组件中调用。
2.2 在组件中读取状态
在组件中调用 Store Hook(如 useCounterStore),并传入一个选择器函数来提取所需的状态片段。组件仅在选择器返回的值变化时重新渲染。
1 | // CounterDisplay.jsx |
如果你需要多个状态字段,可以调用多次 Hook,每次用选择器提取一个基本值。也可以一次返回一个对象,但需要注意——如果返回一个新对象,每次 Store 更新都会触发重新渲染(因为对象引用每次不同)。解决方案是使用 shallow 比较函数。
1 | import { useShallow } from 'zustand/shallow'; |
2.3 在组件中修改状态
修改函数本身也是 Store 的一部分,通过选择器获取它们即可。
1 | // CounterControls.jsx |
注意:修改函数(如 increment)的引用是稳定的——它们在 Store 创建时就已确定,不会在每次渲染时变化。这意味着即使不用 useCallback 包裹,将它们传递给 React.memo 子组件也不会导致不必要的渲染。
2.4 在组件外部访问 Store
Zustand 的 Store 不仅是 React Hook——它还是一个普通的 JavaScript 模块。你可以通过 useCounterStore.getState() 在任何地方(包括非 React 代码、事件处理、异步回调)读取当前状态,通过 useCounterStore.setState() 修改状态。这完全绕过了 React 的渲染周期。
1 | // 在普通 JavaScript 文件中读取和修改 |
这种“脱离组件树”的能力使得 Zustand 非常适合处理非 UI 逻辑——如 WebSocket 消息处理、定时器、第三方 SDK 集成等——这些逻辑可以放在普通的 TypeScript 文件中,通过 getState 和 setState 与 Store 交互,无需通过 React 组件间接访问。
3. 异步 Action
在 Zustand 中,异步操作不需要特殊的中间件。你可以在 create 回调中直接定义一个 async 函数作为修改函数,在其中使用 set 更新状态。
1 | // store.js |
关键行为:
- 异步函数中,
set可以多次调用(分别更新loading、user、error),每次调用都会通知订阅者。这符合直觉,无需像 Redux 那样 dispatch 多个 action。 get可以在异步回调中读取最新状态。例如,在fetchUser中,可以在请求前通过get().user判断是否已有缓存,避免重复请求。
1 | // 组件中使用 |
与 Redux 异步对比:
- Zustand:直接写
async函数,内部调用set。没有 action type、没有 thunk、没有extraReducers。 - Redux Toolkit:使用
createAsyncThunk定义 thunk,在extraReducers中手动处理pending/fulfilled/rejected。更结构化,但代码量更多。
4. 中间件
Zustand 的 Store 可以通过中间件增强。中间件是一个高阶函数,包裹原始的 create 函数。Zustand 内置了三个最常用的中间件。
4.1 persist:持久化到 localStorage
persist 中间件自动将 Store 中的数据同步到 localStorage(或 sessionStorage、AsyncStorage),并在页面刷新后恢复。
1 | import { create } from 'zustand'; |
关键配置:
name:存储在localStorage中的键名,必须唯一,避免不同 Store 的持久化数据冲突。partialize:一个函数,接收完整状态,返回需要持久化的子对象。用于过滤掉不需要持久化的字段(如临时状态、loading、error)。version:可选版本号。当 Store 数据结构发生变化时,递增版本号可触发迁移逻辑。migrate:可选迁移函数,接收旧版本持久化数据和版本号,返回新版本的状态。
4.2 devtools:集成 Redux DevTools
devtools 中间件将 Zustand Store 连接到 Redux DevTools 浏览器扩展,允许你查看状态变化的时间线、手动跳转和调试。
1 | import { create } from 'zustand'; |
set 在 DevTools 模式下的三个参数:
- 部分状态对象或更新函数。
replace:如果为true,完全替换当前状态(而非合并)。默认false。actionName:在 DevTools 中为这次更新显示的名称。强烈建议为每个set调用提供有意义的 action 名称,否则 DevTools 中会显示anonymous,无法区分不同操作。
4.3 immer:简化不可变更新
如果你更喜欢 Redux Toolkit 中 Immer 的“可变”语法,Zustand 也提供了 immer 中间件:
1 | import { create } from 'zustand'; |
4.4 组合多个中间件
Zustand 中间件可以通过函数组合的方式嵌套使用。中间件的应用顺序是从内到外(与函数组合一致):
1 | import { create } from 'zustand'; |
从内到外:immer → persist → devtools。每个中间件包裹下一个中间件增强后的 create 回调。
5. Zustand 与 Redux Toolkit 对比
| 维度 | Zustand | Redux Toolkit |
|---|---|---|
| 概念复杂度 | 极少:Store、set、get |
较多:Store、Reducer、Action、Dispatch、Thunk |
| API 表面积 | 极小,核心仅 create |
中等,configureStore、createSlice、createAsyncThunk 等 |
| 模板代码量 | 几乎为零 | 有(Slice 定义、Store 配置、Provider) |
| 异步处理 | 直接写 async 函数,内部调用 set |
使用 createAsyncThunk,手动处理生命周期 |
| 缓存/数据获取 | 需自行封装或使用第三方库(TanStack Query) | RTK Query 内置,声明式 API 端点 |
| TypeScript | 类型推导极佳,几乎无需手动标注 | 类型推导良好,需少量样板(RootState、AppDispatch) |
| DevTools | 通过 devtools 中间件支持 |
开箱即支持 |
| 中间件生态 | 内置 persist、devtools、immer |
极其丰富(redux-saga、redux-observable 等) |
| 适用场景 | 中小型应用、偏好简洁 API、非 React 环境 | 大型应用、团队协作、需要 RTK Query |
| 包体积 | ~1KB gzipped | ~11KB gzipped(含 RTK Query) |
选型建议:
- 优先 Zustand:中小型项目、新项目探索阶段、偏好极简 API、需要频繁在组件外访问 Store(WebSocket、定时器)。
- 优先 Redux Toolkit:大型团队协作(标准化 reducer 模式利于 Code Review)、需要 RTK Query 的缓存方案、需要中间件生态(如 redux-saga)、与遗留 Redux 代码共存。
- 两者可以共存:例如,使用 Zustand 管理 UI 状态(如主题、模态框开关),使用 RTK Query 管理服务端数据缓存。
6. 综合实战:全局购物车与用户认证
1 | // store.js |
1 | // App.jsx |
设计要点:
- Store 按功能域划分模块(用户、购物车、通用),但都位于同一个
create调用中。Zustand 鼓励小型 Store,必要时可以拆分为多个独立的 Store。 - 所有修改函数使用
set的回调模式(set(state => ...)),确保基于最新状态计算新值。 persist中间件的partialize过滤掉loading和error,避免刷新页面后仍显示“加载中”。devtools中间件的actionName参数为每次状态变更命名,方便在 Redux DevTools 中追踪操作历史。- 组件中使用
useShallow订阅多个字段,避免因返回新对象引用导致不必要的渲染。
课后练习
一、概念自测(选择题 / 填空题)
(单选) Zustand 中,
create回调函数接收的参数是什么?
A.(dispatch, getState)
B.(set, get)
C.(state, action)
D.(store, middleware)(单选) Zustand 的
persist中间件默认将状态持久化到哪里?
A.sessionStorage
B.localStorage
C. IndexedDB
D. Cookie(填空) 在 Zustand 中,要在异步回调里读取最新的状态而不触发重新渲染,应使用
______函数。(多选) 以下关于 Zustand 的描述,哪些是正确的?
A. 使用 Zustand 必须在根组件包裹<Provider>。
B. 异步 action 可以直接定义为async函数,无需额外中间件。
C. Zustand 的修改函数引用是稳定的,可以安全地传递给React.memo子组件。
D. Zustand 可以在组件外通过getState()和setState()操作 Store。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要使用 Zustand 创建一个全局通知管理 Store。要求如下:
- Store 状态包含
notifications数组,每条通知包含{ id, type, message, timestamp }。 - 提供
addNotification(type, message)方法,自动生成唯一 ID 和时间戳,并将通知加入数组。 - 提供
removeNotification(id)方法,根据 ID 删除通知。 - 提供
clearAll()方法,清空所有通知。 - 使用
devtools中间件连接 Redux DevTools,为每个操作命名。 - 使用
persist中间件持久化通知(可选:可以自定义partialize排除临时数据)。 - 创建一个
NotificationCenter组件,显示所有通知,并支持手动关闭单条和清空全部。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Store 和组件的完整代码。提示词中需明确指定中间件的配置方式、操作命名规范以及组件的渲染逻辑。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请使用 Zustand 创建一个全局通知管理系统。需要创建以下文件:
src/store/notificationStore.js:
- 使用
create,导入并组合devtools和persist中间件。- State:
{ notifications: [] }。addNotification(type, message):生成{ id: Date.now(), type, message, timestamp: Date.now() },通过回调形式set(state => ...)加入数组。set第三个参数命名为'notifications/add'。removeNotification(id):通过id过滤掉对应通知。第三个参数'notifications/remove'。clearAll():设置notifications: []。第三个参数'notifications/clear'。persist配置:name: 'notification-storage',partialize: (state) => ({ notifications: state.notifications })。devtools配置:name: 'NotificationStore'。- 导出 Hook
useNotificationStore。src/components/NotificationCenter.jsx:
- 使用
useNotificationStore和useShallow获取notifications、removeNotification、clearAll。- 渲染通知列表:每条通知根据
type('success'绿色、'error'红色、'info'蓝色、'warning'橙色)设置不同背景色。显示消息和相对时间(如“1 分钟前”)。- 每条通知右侧有关闭按钮,点击调用
removeNotification(id)。- 底部有“清空全部”按钮。
- 所有文件添加 JSDoc 注释,确保可直接运行。完成后列出所有文件内容。
四、面试真题与参考答案
题目(腾讯前端面试题):
请对比 Zustand 和 Redux Toolkit 的设计理念和适用场景。在什么情况下 Zustand 的“无 Provider”设计会是一个优势?Zustand 如何在组件外部访问 Store?这种能力适合解决什么问题?请举例说明。
参考答案:
Zustand 的设计理念是极简和透明——状态就是一个普通的 JavaScript 对象,修改函数定义在旁边,无需 Provider 包裹、无需 action type 常量、无需 dispatch。Redux Toolkit 的设计理念是结构化与标准化——通过 Slice、Reducer、Thunk 等明确的概念划分,强制团队按照统一模式编写状态逻辑,适合大型协作项目。Zustand 适合中小型应用、快速原型、或与 Redux 混合使用管理 UI 状态;Redux Toolkit 适合大型应用,尤其是利用 RTK Query 管理服务端数据缓存。
Zustand 的“无 Provider”设计的优势:在多包 monorepo 或微前端场景下,不同子应用可能需要各自独立的 Store,无需共享同一个 Provider 层级。在将 Zustand Store 用于纯逻辑模块(非 React 组件)时,无需模拟 React 环境即可测试和使用。组件树的任何部分都可以按需引入 Store,无需考虑 Provider 的嵌套层级。
Zustand 在组件外部访问 Store 使用 store.getState() 和 store.setState()。这种能力适合解决非 UI 逻辑的状态交互:例如 WebSocket 消息处理(消息到达后直接更新 Store,无需通过组件中转)、定时器回调、第三方 SDK 集成、Node.js 中间件等。举例:一个全局的 WebSocket 管理器,接收到消息后调用 useChatStore.getState().addMessage(msg) 更新聊天记录,无需将 React 组件树暴露给 WebSocket 逻辑。
课后练习答案
一、概念自测答案
B
- 解析:
create((set, get) => ({ ... }))接收set和get两个函数。A 是 ReduxuseReducer的参数;C 是 Redux Reducer 的参数;D 不是 Zustand 的 API。
- 解析:
B
- 解析:
persist中间件默认使用localStorage进行持久化。可通过storage选项修改为sessionStorage或自定义存储引擎。
- 解析:
get- 解析:
get()返回 Store 的最新状态快照,适合在异步回调、定时器或事件处理中读取状态而不触发 React 渲染。
- 解析:
B、C、D
- 解析:A 错误,Zustand 不需要 Provider 包裹组件树。B 正确,异步操作可直接写
async函数。C 正确,create中定义的函数引用在 Store 生命周期中保持稳定。D 正确,getState()和setState()可在组件外使用。
- 解析:A 错误,Zustand 不需要 Provider 包裹组件树。B 正确,异步操作可直接写
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请使用 Zustand 创建一个全局通知 Store 和展示组件。要求:
- Store 使用
create,组合devtools和persist中间件。State 包含notifications数组。- 提供
addNotification(type, message)、removeNotification(id)、clearAll()三个操作,为每个set调用添加 action name。persist的 name 为'notification-storage',partialize仅持久化notifications。NotificationCenter组件:展示所有通知,类型对应不同颜色,支持单条关闭和全部清空。- 使用
useShallow订阅多个字段。输出 store 和组件的完整代码。”