Zustandcombine中间件完全指南:自动推断类型的状态合并方案
【免费下载链接】zustand🐻 Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustand
combine是 Zustand 官方提供的一个状态创建辅助中间件,它通过将"初始状态"与"补充状态与 actions 创建函数"合并,让开发者无需手写任何类型定义即可获得完整的 TypeScript 类型推断。本文基于仓库中的 API 参考文档 编写,并结合 源码实现、类型测试用例及相关指南,深入讲解其签名、底层合并原理、典型使用场景与类型安全边界,帮助你在 React 与纯 vanilla 场景中放心使用这一风格。
一、combine是什么
combine中间件用于创建一个"内聚"的 store:把**初始状态(initial state)与一个补充状态创建函数(additional state creator)**合并在一起。补充函数负责追加新的状态片段(state slices)与 actions。由于最终状态类型由combine自动推导,你完全不需要显式声明interface/type来标注 state 类型。
[!TIP] 使用
combine后,create与createStore的curried(柯里化)版本不再是必需的——这正是它的核心价值之一:让中间件使用更直接、更高效。这一点在 TypeScript 高级指南 中也有说明:combine本身就"创建"了状态,因此无需再用create<State>()(...)的柯里化形式来手动注入类型参数。
二、类型签名(Signature)
combine<T extends object, U extends object>(initialState: T, additionalStateCreatorFn: StateCreator<T, [], [], U>): StateCreator<Omit<T, keyof U> & U, [], []>拆解这一签名(见 combine.ts 源码):
T extends object:initialState的类型,必须是对象类型;U extends object:additionalStateCreatorFn返回值(即补充的状态与 actions)的类型;StateCreator<T, [], [], U>:说明补充函数接收基于状态T的set、get、store参数,并返回U;- 返回类型
StateCreator<Omit<T, keyof U> & U, [], []>:Omit<T, keyof U> & U表示"取T中未被U覆盖的键,再与U求交集",即最终状态是二者的**浅合并(shallow merge)**类型。源码中的type Write<T, U> = Omit<T, keyof U> & U正是这一逻辑的别名。
三、参数与返回值(Reference)
combine(initialState, additionalStateCreatorFn)
参数
initialState:store 的初始值。可以是任意类型的值,唯独不能是函数(函数会被误判为创建函数);additionalStateCreatorFn:一个接收set、get与store三个参数的函数(对应 vanilla 的StateCreator约定)。通常你会在其中返回一个包含想要暴露的方法(actions)的对象。
返回值
返回一个状态创建函数(state creator function),可直接传给create或createStore。
四、运行时实现原理(源码级)
combine的实现极其精简,核心只有一行(src/middleware/combine.ts):
return (...args) => Object.assign({}, initialState, (create as any)(...args))这意味着:
- 先以
{}为基底; - 展开
initialState; - 再执行补充创建函数得到的状态对象,并浅合并覆盖同名字段。
因此:当U中某个键与T相同名时,补充函数的返回值胜出,这与类型签名中Omit<T, keyof U> & U的语义完全一致。最终合并出的对象会被作为完整状态传给底层的 createStoreImpl,后者再把它与setState、getState、subscribe、getInitialState一起组成StoreApi。
另外注意combine返回的创建函数同样透传set、get与store(源码中即...args的完整转发),因此合并后的状态创建函数与普通创建函数在使用上没有任何区别。
五、使用示例:创建自动推断类型的 store
以下示例来自 API 参考文档:用createStore(vanilla 版)追踪指针移动,把圆点定位到鼠标位置,全程无需任何显式类型标注。
import { createStore } from 'zustand/vanilla' import { combine } from 'zustand/middleware' const positionStore = createStore( combine({ position: { x: 0, y: 0 } }, (set) => ({ setPosition: (position) => set({ position }), })), ) const $dotContainer = document.getElementById('dot-container') as HTMLDivElement const $dot = document.getElementById('dot') as HTMLDivElement $dotContainer.addEventListener('pointermove', (event) => { positionStore.getState().setPosition({ x: event.clientX, y: event.clientY, }) }) const render: Parameters<typeof positionStore.subscribe>[0] = (state) => { $dot.style.transform = `translate(${state.position.x}px, ${state.position.y}px)` } render(positionStore.getInitialState(), positionStore.getInitialState()) positionStore.subscribe(render)配套的 HTML:
<div id="dot-container" style="position: relative; width: 100vw; height: 100vh;" > <div id="dot" style="position: absolute; background-color: red; border-radius: 50%; left: -10px; top: -10px; width: 20px; height: 20px;" ></div> </div>要点说明:
combine自动推导出状态类型{ position: { x: number; y: number }; setPosition: (position: { x: number; y: number }) => void },因此setPosition的参数类型、render订阅回调的state.position都具备完整的智能提示;- 订阅回调类型直接借用
Parameters<typeof positionStore.subscribe>[0],保持与 store 类型同步,避免手写漂移; - 这里刻意用
getInitialState()完成首次渲染(订阅前),随后通过subscribe持续响应指针移动。
React 场景下的用法
combine同样适用于 React 的create钩子。参考 TypeScript 入门指南,状态与 actions 分离,类型完全自动推断:
import { create } from 'zustand' import { combine } from 'zustand/middleware' export const useBearStore = create( combine({ bears: 0 }, (set) => ({ increase: () => set((s) => ({ bears: s.bears + 1 })), })), )六、与其他中间件组合
combine是"创建状态型"中间件,可以被其他变换型中间件(如devtools、subscribeWithSelector)包裹使用。仓库的 类型测试 覆盖了多种组合(见devtools & combine、subscribeWithSelector & combine、devtools & subscribeWithSelector & combine等用例,tests/middlewareTypes.test.tsx),例如:
import { create } from 'zustand' import { devtools, subscribeWithSelector, combine } from 'zustand/middleware' const useBoundStore = create( devtools( subscribeWithSelector( combine({ count: 1 }, (set, get) => ({ inc: () => set({ count: get().count + 1 }, false), })), ), ), )注意:像devtools、persist、subscribeWithSelector这类中间件通常通过 curried 形式配合显式状态类型使用;而combine属于"创建状态"的中间件(同类的还有redux),当你使用它们时就不必再写柯里化版本,因为状态已由combine推断出来(见 advanced-typescript.md 中的说明)。如果确实需要在 store 声明之外单独取出状态类型,可以使用ExtractState类型助手:
import { create, ExtractState } from 'zustand' import { combine } from 'zustand/middleware' type BearState = ExtractState<typeof useBearStore> const useBearStore = create( combine({ bears: 0 }, (set) => ({ increase: (by: number) => set((state) => ({ bears: state.bears + by })), })), )(ExtractState定义见 src/vanilla.ts:S extends { getState: () => infer T } ? T : never。)
七、类型推断机制的"小谎言"与注意事项
combine之所以能自动推断,是因为它在类型层面对传入的set、get、store做了巧妙的近似处理(详见 advanced-typescript.md 的警告段落):
- 在补充创建函数内部,
set/get/store被假装类型为"仅初始状态T",而实际最终状态是二者的浅合并{ ...initialState, ...additional }; - 例如
get在函数内的返回类型是() => { bears: number },而真实运行时返回的是{ bears: number, increase: (by: number) => void }; - 由于
{ bears: number }是最终状态的子类型,这种近似在绝大多数场景下是**健全(sound)**的,不会引发问题。
但请警惕以下两个容易踩坑的场景:
set(..., true)全量替换:set({ bears: 0 }, true)会通过编译,但运行时会把increase等函数一并删除,造成状态损坏(底层setState在replace为真时直接整体替换,见 src/vanilla.ts);Object.keys(get()):在补充函数内用Object.keys会返回["bears", "increase"]而不是["bears"],因为运行时对象确实包含 actions,而get的近似返回类型可能让你误判。
此外需要留意:initialState不能是函数;且由于合并是浅合并,嵌套对象(如示例中的position对象)需要整体替换或使用不可变更新,这与 Zustand 一贯的不可变更新约定一致。
八、测试与证据
仓库在 tests/middlewareTypes.test.tsx 中为combine提供了类型级验证:断言useBoundStore((s) => s.count) * 2的类型为number、s.inc()的类型为void,并验证combine({ count: 0 }, () => ({}))的结果仍可赋值给StoreApi<object>。这些用例保证了"无需显式类型、推断结果可用"的核心承诺。
九、Troubleshooting
参考文档中该小节标记为TBD(待补充)。结合源码与指南,可先记住以下排查要点:
- "类型推断失败":确认
initialState不是函数,且补充创建函数返回的是一个对象字面量(而非条件分支导致类型宽泛化); - "运行时多了/少了字段":检查是否用了
set(..., true)全量替换,或Object.keys依赖了get()的字段; - "与持久化配合异常":
combine与persist组合时,持久化的 merge 逻辑以持久化层的配置为准,注意与combine的浅合并语义区分。
总体而言,combine用极小的实现代价换来了"零类型样板代码"的开发体验,非常适合状态结构清晰、希望保持代码简洁的 TypeScript 项目——正如 TypeScript 入门指南 所说,这是一种在 TS 项目中非常流行的写法。
【免费下载链接】zustand🐻 Bear necessities for state management in React项目地址: https://gitcode.com/gh_mirrors/zu/zustand
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考