高级模式
本章涵盖 Zustand 的高级用法:Map/Set 状态管理、Zustand v5 新特性、与 React 外部系统集成、事件总线模式等。
Map 和 Set 在 Store 中
Zustand 的 set 是浅合并,不会自动检测 Map/Set 内部的变化。解决方案:每次都创建新的 Map/Set。
Map 状态
interface User {
id: number;
name: string;
}
interface MapStore {
users: Map<number, User>;
addUser: (user: User) => void;
removeUser: (id: number) => void;
updateUser: (id: number, updates: Partial<User>) => void;
getUser: (id: number) => User | undefined;
}
const useMapStore = create<MapStore>((set, get) => ({
users: new Map(),
addUser: (user) =>
set((s) => {
const next = new Map(s.users); // 创建新 Map
next.set(user.id, user);
return { users: next };
}),
removeUser: (id) =>
set((s) => {
const next = new Map(s.users);
next.delete(id);
return { users: next };
}),
updateUser: (id, updates) =>
set((s) => {
const existing = s.users.get(id);
if (!existing) return {};
const next = new Map(s.users);
next.set(id, { ...existing, ...updates });
return { users: next };
}),
getUser: (id) => get().users.get(id),
}));🚨 陷阱:直接在旧 Map 上
.set()然后用set({ users: oldMap })不会触发重渲染——因为引用没变。必须创建新的 Map 实例。
Set 状态
interface SetStore {
selectedIds: Set<number>;
toggle: (id: number) => void;
selectAll: (ids: number[]) => void;
clear: () => void;
has: (id: number) => boolean;
}
const useSetStore = create<SetStore>((set, get) => ({
selectedIds: new Set(),
toggle: (id) =>
set((s) => {
const next = new Set(s.selectedIds);
next.has(id) ? next.delete(id) : next.add(id);
return { selectedIds: next };
}),
selectAll: (ids) => set({ selectedIds: new Set(ids) }),
clear: () => set({ selectedIds: new Set() }),
has: (id) => get().selectedIds.has(id),
}));使用 Immer 简化 Map/Set 操作
import { immer } from 'zustand/middleware/immer';
const useMapStore = create(
immer((set) => ({
users: new Map(),
addUser: (user) =>
set((s) => {
s.users.set(user.id, user); // ✅ Immer 下直接修改
}),
}))
);💡 最佳实践:如果项目中已使用 Immer 中间件,Map/Set 操作变得和普通对象一样简单。否则需要手动创建新实例。
嵌套对象更新
interface DeepStore {
user: {
profile: {
name: string;
address: {
city: string;
zip: string;
};
};
};
}
// ❌ 繁琐的手动展开
set((s) => ({
user: {
...s.user,
profile: {
...s.user.profile,
address: {
...s.user.profile.address,
city: 'Beijing',
},
},
},
}));
// ✅ 用 Immer 中间件
set((s) => {
s.user.profile.address.city = 'Beijing';
});Zustand v5 新变化
v5 是一次重大的 API 清理,以下是主要变化:
1. 移除默认导出
// ❌ v4:默认导入
import create from 'zustand';
// ✅ v5:命名导入
import { create } from 'zustand';2. 中间件导出路径变化
// ❌ v4
import { persist, devtools } from 'zustand/middleware';
// ✅ v5(一致,但不再从主入口导出)
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';3. persist 的 storage 选项增强
// v4
persist(store, {
getStorage: () => localStorage,
})
// v5: 使用 createJSONStorage 包装
import { createJSONStorage } from 'zustand/middleware';
persist(store, {
storage: createJSONStorage(() => localStorage),
})4. TypeScript 类型更严格
v5 中对 set 的类型推断更准确,减少了不必要的 as 断言。但同时也意味着不正确的类型声明更容易暴露。
5. 移除的 API
| 移除项 | 替代方案 |
|---|---|
default 导出 |
import { create } |
zustand/vanilla 的 createStore |
import { createStore } from 'zustand/vanilla'(仍然可用) |
v4 → v5 迁移清单
// 1. 修改所有导入
- import create from 'zustand'
+ import { create } from 'zustand'
- import { shallow } from 'zustand'
+ import { shallow } from 'zustand/shallow'
// 2. 修改 persist 的 storage
- getStorage: () => localStorage
+ storage: createJSONStorage(() => localStorage)
// 3. 更新 TypeScript 类型(如有必要)
// v5 的类型更严格,可能需要修复一些类型声明
在非 React 环境中使用(Vanilla Store)
Zustand 可以不依赖 React 独立使用:
import { createStore } from 'zustand/vanilla';
interface CounterStore {
count: number;
inc: () => void;
dec: () => void;
}
// createStore 返回的是 vanilla store(非 Hook)
const counterStore = createStore<CounterStore>((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
dec: () => set((s) => ({ count: s.count - 1 })),
}));
// 在任何地方使用(Node.js、Web Worker、非 React 框架)
counterStore.getState().count; // 0
counterStore.getState().inc();
counterStore.getState().count; // 1
// 订阅
const unsub = counterStore.subscribe((state, prev) => {
console.log(`${prev.count} → ${state.count}`);
});将 vanilla store 转为 React Hook
import { useStore } from 'zustand';
import { createStore } from 'zustand/vanilla';
const vanillaStore = createStore((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}));
// 转为 React Hook
function useCounterStore<T>(selector: (s: CounterStore) => T): T {
return useStore(vanillaStore, selector);
}
function Counter() {
const count = useCounterStore((s) => s.count);
return <div>{count}</div>;
}💡 最佳实践:vanilla store 适合作为框架无关的核心逻辑层——比如在 React Native、Vue、Svelte 之间共享同一个状态管理逻辑。
事件总线模式
利用 zustand 的 vanilla API,可以构建轻量级事件总线:
import { createStore } from 'zustand/vanilla';
interface EventBus {
events: Map<string, Set<(...args: any[]) => void>>;
on: (event: string, handler: (...args: any[]) => void) => () => void;
emit: (event: string, ...args: any[]) => void;
once: (event: string, handler: (...args: any[]) => void) => void;
}
const eventBus = createStore<EventBus>((set, get) => ({
events: new Map(),
on: (event, handler) => {
set((s) => {
const next = new Map(s.events);
if (!next.has(event)) next.set(event, new Set());
next.get(event)!.add(handler);
return { events: next };
});
// 返回取消订阅函数
return () => {
set((s) => {
const next = new Map(s.events);
next.get(event)?.delete(handler);
return { events: next };
});
};
},
emit: (event, ...args) => {
const handlers = get().events.get(event);
handlers?.forEach((fn) => fn(...args));
},
once: (event, handler) => {
const wrapped = (...args: any[]) => {
handler(...args);
unsub();
};
const unsub = get().on(event, wrapped);
},
}));
// 使用
eventBus.getState().on('userLogin', (user) => console.log('login:', user));
eventBus.getState().emit('userLogin', { id: 1, name: 'Alice' });💡 最佳实践:事件总线适合跨组件通信的少数场景(如全局通知、WebSocket 消息分发)。大部分场景用多个独立 Store + 跨 Store 调用就够了,不需要引入事件总线。
自动生成选择器(Auto Selector)
对于大型 Store,手动写选择器很繁琐。Zustand 社区提供了自动生成选择器 hook 的方案:
import { create } from 'zustand';
import { StoreApi, UseBoundStore } from 'zustand';
// 工具类型:为 Store 的每个 key 生成对应的 selector hook
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
_store: S
) => {
const store = _store as WithSelectors<typeof _store>;
store.use = {} as any;
for (const k of Object.keys(store.getState())) {
(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
}
return store;
};
// 使用
interface BearState {
bears: number;
fishes: number;
increase: () => void;
}
const useBearStoreBase = create<BearState>()((set) => ({
bears: 0,
fishes: 0,
increase: () => set((s) => ({ bears: s.bears + 1 })),
}));
const useBearStore = createSelectors(useBearStoreBase);
// 自动生成的 selector hooks
function Component() {
const bears = useBearStore.use.bears(); // ← 自动生成
const increase = useBearStore.use.increase();
// ...
}💡 最佳实践:
createSelectors在大型 Store 中很实用。但如果 Store 不太大(< 10 个字段),手写选择器更直观。注意自动选择器不适用于派生值(需要计算的字段)。
受控/非受控 Store 模式
// 非受控模式:每个使用地方各自创建 Store
import { create } from 'zustand';
import { createContext, useContext, useRef } from 'react';
// 创建 Store 的工厂函数
function createCounterStore(initialCount = 0) {
return create<CounterState>((set) => ({
count: initialCount,
inc: () => set((s) => ({ count: s.count + 1 })),
}));
}
// 通过 Context 传递
const CounterContext = createContext<ReturnType<typeof createCounterStore> | null>(null);
function CounterProvider({ children, initialCount = 0 }) {
const storeRef = useRef<ReturnType<typeof createCounterStore>>();
if (!storeRef.current) {
storeRef.current = createCounterStore(initialCount);
}
return (
<CounterContext.Provider value={storeRef.current}>
{children}
</CounterContext.Provider>
);
}
function useCounterContext<T>(selector: (s: CounterState) => T): T {
const store = useContext(CounterContext);
if (!store) throw new Error('Missing CounterProvider');
return store(selector);
}💡 最佳实践:Context + Zustand 的组合提供了依赖注入能力——同一个组件树的不同部分可以有各自的 Store 实例。适合需要多实例的场景(如多个独立的表单、多个图表)。