news 2026/9/18 19:59:37

Zustand redux 中间件详解:用 Redux 式 action/reducer 驱动状态更新

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zustand redux 中间件详解:用 Redux 式 action/reducer 驱动状态更新

Zustand redux 中间件详解:用 Redux 式 action/reducer 驱动状态更新

【免费下载链接】zustand🐻 Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustand

redux是 Zustand 提供的一个官方中间件,它让你沿用 Redux 的心智模型——通过reducer 纯函数type的 action 对象来更新 store,同时保留 Zustand 轻量、无样板代码的 API。本文基于 zustand 仓库的官方文档 docs/reference/middlewares/redux.md 展开,结合 redux 中间件源码 与仓库测试用例,带你掌握redux的签名、类型系统、完整用法,以及与devtools中间件协作进行 Redux DevTools 调试的底层原理。

概览:为什么需要 redux 中间件

Zustand 默认推荐直接用set函数更新状态,但如果你来自 Redux 生态、或团队已经沉淀了一套以 action 和 reducer 为中心的状态管理规范,redux中间件可以无缝承接这套模式:

const nextStateCreatorFn = redux(reducerFn, initialState)

它接收一个reducer 纯函数初始状态,返回一个可供createStore(vanilla)或create(React)消费的状态创建函数。从仓库入口 src/middleware.ts 可以看到,reduxdevtoolspersistcombine等一起从zustand/middleware统一导出。

安装与引入

redux中间件随 Zustand 核心包一起发布,无需额外安装依赖:

import { createStore } from 'zustand/vanilla' // 非 React 环境 import { redux } from 'zustand/middleware'

在 React 组件中则使用zustand主入口的create

import { create } from 'zustand' import { redux } from 'zustand/middleware'

类型签名与 Mutator

Signature(签名)

redux<T, A extends { type: string }>(reducerFn: (state: T, action: A) => T, initialState: T): StateCreator<T & { dispatch: (action: A) => A }, [['zustand/redux', A]], []>

签名要点:

  • 泛型T是状态类型,A是 action 联合类型,且约束为{ type: string }——即每个 action 必须携带type字段;
  • reducerFn类型为(state: T, action: A) => T,输入当前状态与 action,输出新状态;
  • 返回的StateCreator状态类型被扩展为T & { dispatch: (action: A) => A }dispatch接收 action 并原样返回该 action
  • 中间三个类型参数中[['zustand/redux', A]]表示该中间件注册的 mutator,A被记录下来用于类型推断。

Mutator(类型修饰符)

;['zustand/redux', A]

对应源码 src/middleware/redux.ts#L19-L26 中的类型定义:StateCreator<Write<T, ReduxState<A>>, Cms, [['zustand/redux', A]]>。Zustand 通过 src/vanilla.ts 中的Mutate工具类型,把注册过的 mutator 逐层应用到StoreApi上,最终让dispatch正确出现在 store 的公开类型中。

参数详解:reducerFn 与 initialState

redux(reducerFn, initialState)接收两个参数:

参数类型说明
reducerFn(state: T, action: A) => T必须是纯函数:接收应用当前状态和 action 对象,返回应用该 action 后的新状态。相同的(state, action)输入必须产生相同输出,不应产生副作用(如修改参数、发起请求、写 I/O)
initialStateTstore 的初始状态,可以是任意类型的值,但不能是函数

返回值为一个新的状态创建函数(state creator function),可直接传给createStorecreate

从实现层面看(src/middleware/redux.ts#L39-L49),中间件内部把initial展开进初始状态:return { dispatch: ..., ...initial },这正是initialState不能是函数的原因——函数值会被Object.assign式的展开语义破坏,也与 Zustand 对状态值的基本约定冲突。

实战:通过 actions 和 reducers 更新状态

这是官方文档的核心示例:一个"个人信息"表单,三个输入框分别通过三种 action 更新firstNamelastNameemail

定义状态与 action 类型

type PersonStoreState = { firstName: string lastName: string email: string } type PersonStoreAction = | { type: 'person/setFirstName'; firstName: string } | { type: 'person/setLastName'; lastName: string } | { type: 'person/setEmail'; email: string } type PersonStore = PersonStoreState & { dispatch: (action: PersonStoreAction) => PersonStoreAction }

注意 action 类型使用字符串字面量联合'person/setFirstName'等),既满足了A extends { type: string }的约束,也让 reducer 中的switch分支获得穷尽性检查。命名采用域/动作(如person/setFirstName)的 Redux 惯例,便于在 DevTools 中阅读。

编写 reducer 纯函数

const personStoreReducer = ( state: PersonStoreState, action: PersonStoreAction, ) => { switch (action.type) { case 'person/setFirstName': { return { ...state, firstName: action.firstName } } case 'person/setLastName': { return { ...state, lastName: action.lastName } } case 'person/setEmail': { return { ...state, email: action.email } } default: { return state // 未知 action:原样返回,保证纯函数契约 } } }

reducer 遵循三条铁律:

  1. 不可变更新:用展开语法{ ...state, ... }产生新对象,绝不原地修改;
  2. default 分支返回原 state:未知 action 时返回传入的state本身,而不是新对象,避免无意义的订阅通知;
  3. 无副作用:不在此处发起请求、调用setState或写日志。

初始化并创建 store

const personStoreInitialState: PersonStoreState = { firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com', } const personStore = createStore<PersonStore>()( redux(personStoreReducer, personStoreInitialState), )

这里使用createStore<PersonStore>()(...)的柯里化调用形式,显式指定完整 store 类型(含dispatch),让personStore.dispatch获得正确的 action 联合类型提示。

绑定 DOM 事件并订阅渲染

const $firstNameInput = document.getElementById( 'first-name', ) as HTMLInputElement const $lastNameInput = document.getElementById('last-name') as HTMLInputElement const $emailInput = document.getElementById('email') as HTMLInputElement const $result = document.getElementById('result') as HTMLDivElement function handleFirstNameChange(event: Event) { personStore.dispatch({ type: 'person/setFirstName', firstName: (event.target as any).value, }) } function handleLastNameChange(event: Event) { personStore.dispatch({ type: 'person/setLastName', lastName: (event.target as any).value, }) } function handleEmailChange(event: Event) { personStore.dispatch({ type: 'person/setEmail', email: (event.target as any).value, }) } $firstNameInput.addEventListener('input', handleFirstNameChange) $lastNameInput.addEventListener('input', handleLastNameChange) $emailInput.addEventListener('input', handleEmailChange) const render: Parameters<typeof personStore.subscribe>[0] = (person) => { $firstNameInput.value = person.firstName $lastNameInput.value = person.lastName $emailInput.value = person.email $result.innerHTML = `${person.firstName} ${person.lastName} (${person.email})` } render(personStore.getInitialState(), personStore.getInitialState()) personStore.subscribe(render)

配合的 HTML 结构:

<label style="display: block"> First name: <input id="first-name" /> </label> <label style="display: block"> Last name: <input id="last-name" /> </label> <label style="display: block"> Email: <input id="email" /> </label> <p id="result"></p>

工作流程闭环:

  1. 用户在输入框键入 →input事件触发;
  2. 事件处理器构造 action 并调用personStore.dispatch(action)
  3. 中间件把 action 交给 reducer,得到新状态并写入 store;
  4. store 通知所有订阅者,render函数更新输入框与结果文本。

render(personStore.getInitialState(), personStore.getInitialState())用于首屏初始化:subscribe的回调签名是(state, prevState),这里用getInitialState()获取初始状态完成首次渲染,随后每次dispatch都会驱动重绘。

源码深潜:redux 中间件是如何工作的

redux的实现非常精简,核心只有十几行(src/middleware/redux.ts#L39-L49):

const reduxImpl: ReduxImpl = (reducer, initial) => (set, _get, api) => { type S = typeof initial type A = Parameters<typeof reducer>[1] ;(api as any).dispatch = (action: A) => { ;(set as NamedSet<S>)((state: S) => reducer(state, action), false, action) return action } ;(api as any).dispatchFromDevtools = true return { dispatch: (...args) => (api as any).dispatch(...args), ...initial } }

拆解其调用链:

  1. 柯里化返回redux(reducer, initial)返回一个(set, get, api) => ...函数,符合StateCreator约定。set来自 createStore 实现,是 Zustand 的底层状态写入函数;
  2. dispatch 挂到 api 上api.dispatch(action)内部把 action 交给 reducer 计算新状态——set((state) => reducer(state, action), false, action)。这里第二个参数false表示非替换模式,新状态会与旧状态浅合并(Zustand 默认行为,对应 src/vanilla.ts#L66-L81 中replace ?? (typeof nextState !== 'object' ...)的判断逻辑);第三个参数action是供devtools中间件读取的"动作名";
  3. dispatch 返回值:reducer 计算出新状态后,dispatch原样返回传入的 action——这与 Redux 的dispatch语义一致,方便调用方链式使用;
  4. dispatchFromDevtools 标记(api as any).dispatchFromDevtools = true是一个内部协作标志,告诉devtools中间件"这个 store 自带 dispatch,可以从 DevTools 面板派发 action";
  5. 状态创建函数返回{ dispatch: (...args) => api.dispatch(...args), ...initial }dispatch与初始状态一起作为 store 的初始内容返回,createStore会把这个返回值当作初始 state。

与 devtools 中间件组合:获得 Redux DevTools 调试能力

redux中间件最强大的用法之一,是与devtools中间件组合,让 store 接入 Redux DevTools 的 time-travel 调试。仓库测试 tests/devtools.test.tsx#L741-L780 完整验证了这一组合:

import { createStore } from 'zustand/vanilla' import { devtools, redux } from 'zustand/middleware' const api = createStore( devtools( redux( ( { count }, { type }: { type: 'INCREMENT' } | { type: 'DECREMENT' }, ) => ({ count: count + (type === 'INCREMENT' ? 1 : -1), }), { count: 0 }, ), { enabled: true }, ), ) api.dispatch({ type: 'INCREMENT' }) api.dispatch({ type: 'INCREMENT' }) // DevTools 面板中会依次记录两条 action,状态从 { count: 0 } → { count: 2 }

协作机制(源码见 src/middleware/devtools.ts):

  • redux设置的api.dispatchFromDevtools = true被 shouldDispatchFromDevtools 检测到后,devtools会把从面板收到的ACTION消息转交给api.dispatch(action)(src/middleware/devtools.ts#L350-L352),从而在 DevTools 中"回放"你的 reducer 逻辑;
  • devtools包装setState时把redux传入的 action 对象作为第三个参数读取,用于在 DevTools 中标注动作类型,因此 DevTools 面板里显示的是person/setFirstName这类有意义的动作名,而非匿名动作;
  • 面板的JUMP_TO_STATEJUMP_TO_ACTIONROLLBACKIMPORT_STATE等指令,则绕过dispatch直接通过setStateFromDevtools写入历史状态(src/middleware/devtools.ts#L372-L396),实现真正的 time-travel。

需要提醒:使用devtools中间件需额外安装@redux-devtools/extension库,且enabled默认仅在开发环境(import.meta.env?.MODE !== 'production')启用(src/middleware/devtools.ts#L200-L203)。

类型层面的细节:Write 与模块扩展

redux的类型实现中有一个关键工具类型:

type Write<T, U> = Omit<T, keyof U> & U

Write<S, ReduxState<A>>ReduxState<A>(即{ dispatch: (action: A) => A })并入原状态类型,若原类型中已有同名属性则以新类型为准。这保证了无论T是什么,dispatch的签名都由中间件接管。

同时,源码通过 TypeScript 的模块扩充为 Zustand 注册 mutator(src/middleware/redux.ts#L28-L32):

declare module '../vanilla' { interface StoreMutators<S, A> { 'zustand/redux': WithRedux<S, A> } }

这样createStore/create在类型层面就能识别redux的存在,并自动把dispatch暴露到 store 的 API 类型上。

仓库的类型测试 tests/middlewareTypes.test.tsx#L118-L151 验证了这些类型契约:

  • useBoundStore((s) => s.count) * 2推断为number,且s上能访问dispatch
  • useBoundStore((s) => s.dispatch)({ type: 'INC' })返回类型为{ type: 'INC' }——印证 dispatch 返回传入的 action;
  • 无 selector 的useBoundStore().dispatch(...)与绑定在 store 对象上的useBoundStore.dispatch(...)同样可用(React 版create会把 store API 合并到 hook 上,见 src/react.ts#L53-L61)。

在 React 组件中使用

vanilla 版示例之外,在 React 中只需把createStore换成create

import { create } from 'zustand' import { redux } from 'zustand/middleware' type CounterState = { count: number } type CounterAction = { type: 'INC' } | { type: 'DEC' } const useCounterStore = create( redux<CounterState, CounterAction>( (state, action) => { switch (action.type) { case 'INC': return { ...state, count: state.count + 1 } case 'DEC': return { ...state, count: state.count - 1 } default: return state } }, { count: 0 }, ), ) function Counter() { const count = useCounterStore((s) => s.count) const dispatch = useCounterStore((s) => s.dispatch) return ( <> <p>{count}</p> <button onClick={() => dispatch({ type: 'INC' })}>+</button> <button onClick={() => dispatch({ type: 'DEC' })}>-</button> </> ) }

create返回的 hook 同时挂载了 store 的完整 API(src/react.ts#L58 的Object.assign(useBoundStore, api)),因此在组件外也可以直接用useCounterStore.dispatch(...)派发 action。

中间件组合顺序

redux可以与persist等中间件自由组合,顺序遵循 Zustand 惯例——先写外层中间件,让后续(内层)中间件在状态写入链路中更靠近底层:

import { create } from 'zustand' import { devtools, persist, redux } from 'zustand/middleware' const useStore = create( devtools( persist( redux(reducerFn, initialState), { name: 'app-storage' }, ), { name: 'app-store' }, ), )

此时调用链为:dispatchredux的 reducer 计算 →persist的存储同步 →devtools的记录上报,三方各司其职。注意redux需要置于devtools内层(如上例),才能让 DevTools 正确识别dispatchFromDevtools标志;若把devtools放在redux内部,则无法享受 action 回放能力。

常见问题排查(Troubleshooting)

官方文档的 Troubleshooting 一节仍标记为 TBD,以下结论依据源码实现与测试用例整理:

1. dispatch 在 store 上不可用检查是否真的用了redux中间件——redux返回的初始状态对象中带有dispatch(src/middleware/redux.ts#L48),createStore会把它作为初始 state 的一部分(src/vanilla.ts#L95 的initialState)。若你在create的柯里化调用中漏掉(),会因类型不匹配而报错。

2. DevTools 面板中动作名为 anonymous 或完全看不到 action确认devtools位于redux外层、且已安装@redux-devtools/extension;生产环境下enabled默认关闭,可用{ enabled: true }显式开启(测试 tests/devtools.test.tsx#L761 即如此)。动作名来自redux传给set的第三个参数 action 对象,若 action 缺少type字段(不满足A extends { type: string }约束),会退化为匿名动作。

3. dispatch 返回值疑惑dispatch返回的是传入的 action 本身,不是新状态。测试 tests/middlewareTypes.test.tsx#L135-L141 明确断言了返回类型。若要读取最新状态,请用getState()或订阅。

4. 状态没有更新最常见原因是 reducer 的default分支返回了新的对象而非原state(导致无意义的通知),或 reducer 内部原地修改了state后返回同一引用——Zustand 的setStateObject.is判断引用是否变化(src/vanilla.ts#L73),引用不变则不会触发更新。

小结

redux中间件以不到 20 行的实现(src/middleware/redux.ts),把 Redux 的 action/reducer 模式完整嫁接到 Zustand 上:纯函数 reducer 保证状态变更可预测,dispatch提供统一的动作入口,dispatchFromDevtools标志让 Redux DevTools 的 time-travel 调试开箱即用,而精心设计的类型签名让 action 联合类型在编译期就得到穷尽性保障。无论是希望渐进式迁移 Redux 存量代码,还是在 Zustand 项目中引入更规范的状态变更约束,它都是值得优先考虑的选择。更多配套示例可参考 docs/learn/guides/flux-inspired-practice.md 与 docs/reference/middlewares/devtools.md。

【免费下载链接】zustand🐻 Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustand

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 19:57:42

概要设计与详细设计:从定骨架到填血肉的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 19:57:13

酒店员工管理系统自研实践:排班考勤与数据库设计

简介&#xff1a;一份针对酒店员工管理系统的计算机专业毕业论文设计文档&#xff0c;适合计算机相关专业学生参考选题、结构撰写与系统开发实现。内容围绕SSM框架、MySQL数据库、HTML及B/S架构展开&#xff0c;完整覆盖课题背景、可行性分析、系统流程图、用例图、数据库设计、…

作者头像 李华
网站建设 2026/9/18 19:56:56

游戏压缩包解压报错怎么办?7-Zip、WinRAR、Bandizip搭配使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 19:56:11

游戏背包系统设计与性能优化实践:数据结构、UI刷新与存档策略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 19:55:25

Scratch到Python:用3D跑酷项目打通编程认知跃迁

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华