文章目录
概念
在调用 create()
或者 createWithEqualityFn()
,要传入一个工厂函数,Zustand 会为这个工厂函数注入两个工具参数
const useStore = create<StoreState>()(
(set, get, api) => { /* 这里 return 状态对象 */ }
);
参数 | 作用 |
---|---|
set | 唯一推荐 的更新状态入口。可批量更新、支持回调、可选 replace 模式。 |
get | 读取当前最新的 store 状态。 |
api | store 实例本身,包含 setState / getState / subscribe 等底层方法。 |
set
的类型签名
type SetState<S> = {
(partial: Partial<S> | ((state: S) => Partial<S | void>), replace?: boolean): void
}
常用用法
局部更新
increase: () => set((state) => ({ count: state.count + 1 })),
- 传入 对象片段 或 返回对象的回调,Zustand 会把它 浅合并 (Object.assign) 到旧状态
- 组件只会因 “真正变更的字段” 触发渲染
一次更新多个字段
set((state) => ({
bears: state.bears + 1,
lastUpdated: Date.now(),
}))
整体替换
// 把整个 store 替换成一个全新的对象
resetAll: () => set({ bears: 0, fish: 0 }, true),
当第二个参数为 true 时,Zustand 不再做浅合并,而是把原状态整体替换成新的对象
异步或带副作用的场景
fetchUser: async (id: string) => {
set({ loading: true });
try {
const user = await api.get(`/user/${id}`);
set({ user, loading: false });
} catch (e) {
set({ error: e, loading: false });
}
},
set
没有对异步做限制:可以 await
或在异步回调里调用它。
Zustand 的柯里化函数问题
在了解中间件之前需要了解什么是柯里化函数
create<AuthState>()( /* 中间件包裹函数 */ )
是一种函数柯里化(curried function)+ 泛型传参的组合语法
create
的定义简化后大致是 function create<T>(): (initializer: StateCreator<T>) => UseBoundStore<T>
也可以看成
const createStore = <T>() => (initializer: (set, get) => T) => {
return /* Hook that uses that initializer */;
}
create<T>()
先把泛型类型 T 提交进来- 之后返回一个函数,这个函数接受初始化函数(即
(set, get) => {...}
) - 最后返回一个可用的 store hook(如
UseBoundStore
)
为什么不是 create<AuthState>(...)
因为 create(…) 需要泛型信息,但不能直接从 (…) 中的中间件推导出来泛型参数。所以必须先传入 <AuthState>
,再调用一次函数传入初始化器
// ❌ 无法推导中间层中间件的返回值类型
create<AuthState>(
persist(...) // ⛔ TypeScript 报错
);
// ✅ 拆成两步,类型信息先注入,再交给中间件返回 store 初始化逻辑
create<AuthState>()(
persist(...) // ✅ 类型可推导
);
这是由于 TypeScript 的 类型推导边界 问题
中间件产生情况
中间件(devtools
/ persist
/ immer
…)都是高阶函数
// 形态示意
const wrappedInitializer = devtools(
persist(
immer(originalInitializer),
persistOptions
),
devtoolsOptions
);
输入 原始 initializer,输出 “增强版 initializer”。
因此 必须出现在 “initializer 要交给 create 的位置” —— 也就是 第二个括号里
运行时情况
- 第一步:等待
init
的函数 —— 完全无运行时开销
const _createStore = create<AuthState>();
// _createStore 的类型: (init: StateCreator<AuthState>) => UseBoundStore<AuthState>
- 第二步
- 中间件先把真
initializer
包装增强 - 完成后交给
_createStore
- 最终得到绑定 React 的
useAuth Hook
- 中间件先把真
const useAuth = _createStore(
devtools(persist((set) => ({ /* ... */ })))
);
总结而言,两对括号只是 类型和调用顺序的语法糖,并不会额外执行 create 两次
中间件的交互
中间件 | 与 set 的关系 |
---|---|
devtools | Wrapper 会把 set 改写成 set(devtoolsWrapper(…)),无需改变写法;每次调用 set 都能在 Redux DevTools 里看到 action。 |
persist | set 仍然用浅合并;被标记为 partialize 的字段会落盘。 |
subscribeWithSelector | 不改 set,但能让外部监听到每次 set 的结果 slice。 |
immer | 把传给 set 的回调参数 state 变成可变草稿,回调结束后由 Immer 产出不可变的新对象 |
devtools
让 Zustand 自动出现在 Redux DevTools,包装 set,在每次状态更新时向 Redux DevTools 发送一条 action 记录
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface CounterState {
count: number;
inc: () => void;
dec: () => void;
}
export const useCounter = create<CounterState>()(
devtools(
(set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 }), false, 'inc'), // 手动命名 action
dec: () => set((s) => ({ count: s.count - 1 }), false, 'dec'),
}),
{ name: 'CounterStore' },
),
);
- 第三个参数 ‘inc’ / ‘dec’ 是 自定义
actionType
,方便在 DevTools 面板里区分 - 如果想在生产环境关闭 DevTools 上报,
process.env.NODE_ENV === 'development' ? devtools(...) : (fn) => fn
persist
把状态持久化到 LocalStorage、IndexedDB 等,拦截每次 set
后的最新状态,将指定字段序列化保存;页面刷新后自动复原
- 默认使用 localStorage (Web)、AsyncStorage (React Native)
- 通过
partialize
精确控制保存范围
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthState {
user: string | null;
token: string | null;
setAuth: (u: string, t: string) => void;
logout: () => void;
}
export const useAuth = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
setAuth: (u, t) => set({ user: u, token: t }),
logout: () => set({ user: null, token: null }),
}),
{
name: 'auth-storage',
partialize: (state) => ({ token: state.token }), // 仅保存 token
version: 1, // 支持升级迁移
// storage: createJSONStorage(() => sessionStorage) // 如需改存储介质
},
),
);
- 仅 token 被序列化;user 页面刷新后会回到 null
- 当调用
setAuth
或logout
时,persist 会在内部二次 set 到存储
immer
把传给 set 的回调参数 state 变成可变草稿,回调结束后由 Immer 产出不可变的新对象
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface Todo {
id: number;
done: boolean;
text: string;
}
interface TodoState {
todos: Todo[];
toggle: (id: number) => void;
}
export const useTodo = create<TodoState>()(
immer((set) => ({
todos: [],
toggle: (id) =>
set((state) => {
const t = state.todos.find((i) => i.id === id);
if (t) t.done = !t.done; // ← 像写普通 JS 一样“改”数据
}),
})),
);
set
回调里直接 修改state.todos
;Immer
自动生成新状态- 如果你想整体替换(例如排序),仍可直接赋新数组:
state.todos = newArr
中间件执行顺序
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { persist } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
// ⬇️ 最推荐的组合写法
export const usePrice = create<PriceState>()(
devtools(
persist(
immer((set) => ({
price: 0,
inc: (n = 1) => set((s) => void (s.price += n)),
reset: () => set({ price: 0 }),
})),
{
name: 'price-storage',
partialize: (s) => ({ price: s.price }),
},
),
{ name: 'PriceStore' },
),
);
immer
先帮我们把深层修改转成不可变新对象- 新状态传给
persist
,它把选中的字段序列化落盘 - 最后 devtools 把这次
set
记录到 Redux DevTools
综合案例
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface PriceState {
price: number;
history: number[];
increase: (n?: number) => void;
undo: () => void;
}
export const usePriceState = create<PriceState>()(
devtools(
persist(
immer((set, get) => ({
price: 0,
history: [],
increase: (n = 1) =>
set((state) => {
state.history.push(state.price); // Immer 可写
state.price += n;
}),
undo: () =>
set((state) => {
const prev = state.history.pop();
if (prev !== undefined) state.price = prev;
}),
})),
{ name: 'price-storage', partialize: s => ({ price: s.price }) },
)
)
);