Store 详解
本章深入 Zustand Store 的创建方式、状态操作 API 和组织模式。
set 的完整用法
基础:传入对象(浅合并)
const useStore = create((set) => ({
name: 'Alice',
age: 30,
setName: (name: string) => set({ name }), // 只更新 name
setAge: (age: number) => set({ age }), // 只更新 age
}));每次 set({ name }) 都是浅合并:只覆盖传入的字段,其他字段保持不变。
函数式更新(依赖前值)
const useCounter = create<{ count: number; inc: () => void }>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })), // 基于当前值
// 连续三次 inc() 会得到 3,而不是 1
}));🚨 陷阱:永远不要在
set外部读取 state 来构建新状态:// ❌ 错误:闭包中的 state 可能是旧的 inc: () => set({ count: state.count + 1 }) // state 从哪来? // ✅ 正确:使用 set 的回调形式 inc: () => set((state) => ({ count: state.count + 1 }))
replace:替换整个 state
// 第二个参数为 true → 完全替换,不合并
set({ count: 0, name: 'Bob' }, true);
// 整个 state 变成 { count: 0, name: 'Bob' },之前的所有字段都丢失
这个能力让 Zustand 可以轻松支持状态重置:
interface AppState {
user: User | null;
theme: 'light' | 'dark';
// ...
}
const initialState: AppState = { user: null, theme: 'light' };
const useAppStore = create<AppState & { reset: () => void }>((set) => ({
...initialState,
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
// 一键重置所有状态
reset: () => set(initialState, true), // replace = true
}));Action name(DevTools 友好)
// 第三个参数在 DevTools 中作为 action name 显示
inc: () =>
set(
(state) => ({ count: state.count + 1 }),
false, // replace(默认 false)
'increment' // action name
),get 的完整用法
get() 返回最新的 state 快照,是同步的:
const useStore = create<{
count: number;
threshold: number;
inc: () => void;
isAboveThreshold: () => boolean;
}>((set, get) => ({
count: 0,
threshold: 10,
inc: () => set((s) => ({ count: s.count + 1 })),
isAboveThreshold: () => get().count > get().threshold,
}));get vs set 回调的 state 参数
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 基于当前值计算新值 | set((s) => ({...})) |
保证拿到最新值 |
| 条件判断 / 副作用 | get() |
不需要修改状态 |
| 跨 action 共享逻辑 | get() |
代码更清晰 |
const useCartStore = create((set, get) => ({
items: [],
addItem: (item) =>
set((state) => ({
// 需要在 set 里基于 items 计算 → 用 state 参数
items: [...state.items, item],
})),
checkout: () => {
// 需要读取多个字段做条件判断 → 用 get()
const { items } = get();
if (items.length === 0) {
alert('Cart is empty!');
return;
}
// ...
},
}));subscribe 的完整用法
const useStore = create((set) => ({ count: 0 }));
// 监听所有变化
const unsub1 = useStore.subscribe((state, prevState) => {
if (state.count !== prevState.count) {
console.log('count changed');
}
});
// 配合 subscribeWithSelector 中间件监听单个字段
import { subscribeWithSelector } from 'zustand/middleware';
const useStore2 = create(
subscribeWithSelector((set) => ({ count: 0, name: 'Alice' }))
);
// 只监听 count 的变化
const unsub2 = useStore2.subscribe(
(state) => state.count, // selector
(count, prevCount) => { // callback
console.log(`count: ${prevCount} → ${count}`);
},
{ equalityFn: shallow, // 可选:自定义比较函数
fireImmediately: true } // 可选:立即触发一次
);💡 最佳实践:
subscribe的典型场景是日志记录、持久化同步、与外部系统(如 WebSocket、Canvas)的桥接。组件中应始终使用 Hook 选择器而非subscribe。
destroy — 销毁 Store
// 移除所有 listener,清理订阅
useStore.destroy();典型场景:测试中重置 store,或在微前端/multi-app 场景下卸载模块时清理。
setState 和 getState — 外部 API
这两个方法在 React 组件外使用(属于 vanilla API):
// 读取状态
const state = useStore.getState();
// 直接设置状态(等价于 store 内部的 set)
useStore.setState({ count: 10 });
// 函数式更新
useStore.setState((prev) => ({ count: prev.count + 1 }));
// 替换整个 state
useStore.setState(initialState, true);💡 最佳实践:
setState是绕过 action 直接修改状态的逃生舱。在测试中用它重置状态,在 WebSocket 等外部回调用它同步数据。但正常业务逻辑应通过 action 修改,保持可追踪性。
Store 组织模式
模式 1:单一巨型 Store(不推荐)
// ❌ 反模式:所有状态堆在一起
const useAppStore = create((set) => ({
user: null,
cart: [],
theme: 'light',
todos: [],
// 20+ actions...
}));模式 2:按功能域拆分(推荐)
// stores/useUserStore.ts
export const useUserStore = create((set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}));
// stores/useCartStore.ts
export const useCartStore = create((set, get) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));
// stores/useThemeStore.ts
export const useThemeStore = create((set) => ({
theme: 'light' as const,
toggle: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}));💡 最佳实践:Zustand 按功能域拆分 Store(每个 Store 独立),而不是把所有状态塞进一个 Store。这与 Redux 的单一 Store 哲学不同。多 Store 模式天然隔离了状态域,避免了不必要的重渲染。
模式 3:跨 Store 通信
// 一个 Store 的 action 中可以操作另一个 Store
const useCartStore = create((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
clear: () => set({ items: [] }),
}));
const useUserStore = create((set) => ({
user: null,
logout: () => {
set({ user: null });
// 登出时清空购物车
useCartStore.getState().clear();
},
}));🚨 陷阱:跨 Store 直接调用要留意循环依赖。如果 Store A 依赖 Store B 的 action,且 Store B 也依赖 Store A,在模块初始化时可能出问题。遇到这种情况考虑提取共享逻辑到第三个 Store 或工具函数。
Slice 模式
当功能复杂时,可以将 Store 拆成 slices,每个 slice 是一个独立的 creator 函数:
import { create, StateCreator } from 'zustand';
// ---- slice 1: 熊相关 ----
interface BearSlice {
bears: number;
addBear: () => void;
eatFish: () => void;
}
const createBearSlice: StateCreator<BearSlice> = (set) => ({
bears: 0,
addBear: () => set((s) => ({ bears: s.bears + 1 })),
eatFish: () => set((s) => ({ bears: s.bears + 1 })),
});
// ---- slice 2: 鱼相关 ----
interface FishSlice {
fishes: number;
addFish: () => void;
}
const createFishSlice: StateCreator<FishSlice> = (set) => ({
fishes: 0,
addFish: () => set((s) => ({ fishes: s.fishes + 1 })),
});
// ---- 组合 ----
interface BoundStore extends BearSlice, FishSlice {}
const useStore = create<BoundStore>()((...args) => ({
...createBearSlice(...args),
...createFishSlice(...args),
}));Slice 间相互依赖
// 鱼 slice 需要访问熊的 bears
const createFishSlice: StateCreator<
BoundStore, // 完整类型
[], // 中间件
[], // 无额外依赖
FishSlice // 返回类型
> = (set) => ({
fishes: 0,
addFish: () => set((s) => ({ fishes: s.fishes + 1 })),
// 跨 slice 读取
report: () => {
const state = get() as BoundStore; // 需要 as 断言
console.log(`Bears: ${state.bears}`);
},
});💡 最佳实践:Slice 模式适合大型应用中的团队协作(每个 slice 一个文件,各自维护)。小型项目直接分多个独立 Store 就够了,不需要这种复杂度。
create 的 api 参数
create 的 creator 函数还有第三个参数 api,它暴露了 store 的底层 API:
const useStore = create((set, get, api) => ({
count: 0,
// api 就是 store 本身
// api.getState() === useStore.getState()
// api.setState(...) === useStore.setState(...)
// api.subscribe(...) === useStore.subscribe(...)
// api.destroy() === useStore.destroy()
// 通常不需要用 api,除非你要在创建 store 时注册副作用
}));
// api 是同一个引用——可以提前暴露出去
大多数场景不需要使用 api 参数,它主要为高级场景(如创建自定义中间件)预留。