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 可以看到,redux与devtools、persist、combine等一起从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) |
initialState | T | store 的初始状态,可以是任意类型的值,但不能是函数 |
返回值为一个新的状态创建函数(state creator function),可直接传给createStore或create。
从实现层面看(src/middleware/redux.ts#L39-L49),中间件内部把initial展开进初始状态:return { dispatch: ..., ...initial },这正是initialState不能是函数的原因——函数值会被Object.assign式的展开语义破坏,也与 Zustand 对状态值的基本约定冲突。
实战:通过 actions 和 reducers 更新状态
这是官方文档的核心示例:一个"个人信息"表单,三个输入框分别通过三种 action 更新firstName、lastName、email。
定义状态与 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 遵循三条铁律:
- 不可变更新:用展开语法
{ ...state, ... }产生新对象,绝不原地修改; - default 分支返回原 state:未知 action 时返回传入的
state本身,而不是新对象,避免无意义的订阅通知; - 无副作用:不在此处发起请求、调用
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>工作流程闭环:
- 用户在输入框键入 →
input事件触发; - 事件处理器构造 action 并调用
personStore.dispatch(action); - 中间件把 action 交给 reducer,得到新状态并写入 store;
- 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 } }拆解其调用链:
- 柯里化返回:
redux(reducer, initial)返回一个(set, get, api) => ...函数,符合StateCreator约定。set来自 createStore 实现,是 Zustand 的底层状态写入函数; - 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中间件读取的"动作名"; - dispatch 返回值:reducer 计算出新状态后,
dispatch原样返回传入的 action——这与 Redux 的dispatch语义一致,方便调用方链式使用; - dispatchFromDevtools 标记:
(api as any).dispatchFromDevtools = true是一个内部协作标志,告诉devtools中间件"这个 store 自带 dispatch,可以从 DevTools 面板派发 action"; - 状态创建函数返回:
{ 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_STATE、JUMP_TO_ACTION、ROLLBACK、IMPORT_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> & UWrite<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' }, ), )此时调用链为:dispatch→redux的 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 的setState用Object.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),仅供参考