中间件
中间件是 Zustand 的扩展机制,用于给 Store 添加通用能力(如日志、持久化、DevTools、不可变更新等)。
中间件原理
中间件是一个高阶函数,包裹 Store 的 creator:
// 伪代码:中间件本质
function myMiddleware(creator) {
// 返回一个新的 creator,它接收 set, get, api
return (set, get, api) => {
// 可以增强 set / get / api
const enhancedSet = (partial, replace, name) => {
console.log('set called:', partial); // 副作用
set(partial, replace, name); // 调用原始 set
};
return creator(enhancedSet, get, api);
};
}多个中间件可以组合——从外到内包裹:
create(
devtools( // ← 最外层
persist( // ← 第二层
immer( // ← 最内层(最靠近 creator)
(set, get) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
})
)
)
)
);组合顺序一般不影响功能,但 persist 在最内层时持久化的数据不会经过 devtools 的序列化逻辑。推荐把 immer 放最内层(直接包裹 creator)。
内置中间件一览
| 中间件 | 导入路径 | 用途 |
|---|---|---|
devtools |
zustand/middleware |
连接 Redux DevTools |
persist |
zustand/middleware |
localStorage / sessionStorage / AsyncStorage 持久化 |
immer |
zustand/middleware/immer |
用可变语法更新 state |
subscribeWithSelector |
zustand/middleware |
subscribe 支持选择器参数 |
devtools — Redux DevTools 调试
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface BearState {
bears: number;
increase: () => void;
reset: () => void;
}
const useBearStore = create<BearState>()(
devtools(
(set) => ({
bears: 0,
increase: () =>
set(
(s) => ({ bears: s.bears + 1 }),
false,
'increase' // ← action name(显示在 DevTools 中)
),
reset: () => set({ bears: 0 }, false, 'reset'),
}),
{
name: 'BearStore', // DevTools 中显示的名字
enabled: import.meta.env.DEV, // 仅开发环境开启
anonymousActionType: '🦄', // 无名称 action 的默认标题
store: '🐻 Bear Store', // 自定义 store 标识
}
)
);DevTools 中可查看的信息
- State:当前 store 的完整状态树
- Actions:每次
set()调用,可时间旅行回溯 - Diff:每次 action 前后的状态差异
- Trace:调用 action 的组件堆栈(需开启
trace: true)
💡 最佳实践:给每个重要的
set()调用提供第三个参数(action name),在 DevTools 中更好辨认。anonymousActionType给所有未命名的 action 一个默认名称。
persist — 持久化到存储
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface SettingsState {
theme: 'light' | 'dark';
fontSize: number;
setTheme: (theme: 'light' | 'dark') => void;
setFontSize: (size: number) => void;
}
const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
fontSize: 14,
setTheme: (theme) => set({ theme }),
setFontSize: (fontSize) => set({ fontSize }),
}),
{
name: 'app-settings', // localStorage key
// 方式 1:使用 localStorage(默认)
// storage: createJSONStorage(() => localStorage),
// 方式 2:使用 sessionStorage
// storage: createJSONStorage(() => sessionStorage),
// 方式 3:React Native 的 AsyncStorage
// storage: createJSONStorage(() => AsyncStorage),
// 方式 4:自定义存储(如 IndexedDB)
// storage: myCustomStorage,
// 只持久化部分字段
partialize: (state) => ({
theme: state.theme, // 只保留 theme
// fontSize 不持久化
}),
// 从存储恢复到 Store 时的回调
onRehydrateStorage: (state) => {
console.log('开始恢复...', state);
// 返回一个函数,在恢复完成后调用
return (state, error) => {
if (error) {
console.error('恢复失败:', error);
} else {
console.log('恢复完成:', state);
}
};
},
// 版本管理:当 Store 结构变化时,用 migrate 迁移数据
version: 1, // 当前版本
migrate: (persistedState, version) => {
// persistedState 是从存储读出的旧数据
if (version === 0) {
// 旧版没有 theme,给个默认值
return { ...persistedState, theme: 'light' } as SettingsState;
}
return persistedState as SettingsState;
},
// 跳过 hydration(等异步操作完成后再 hydrate)
// skipHydration: true,
// 合并策略:默认浅合并,可自定义
// merge: (persisted, current) => ({ ...current, ...persisted }),
}
)
);手动触发持久化
// 当 skipHydration: true 时,手动触发
useSettingsStore.persist.rehydrate();
// 检查是否已完成 hydration
if (useSettingsStore.persist.hasHydrated()) {
// ...
}
// 手动清除持久化数据
useSettingsStore.persist.clearStorage();partialize 实战
// 只持久化用户偏好,不持久化临时状态
partialize: (state) => ({
theme: state.theme,
lang: state.lang,
// 不包含: isLoading, error, ... 等临时状态
}),🚨 陷阱:
persist初始渲染时 Store 中还是初始值,hydration 是异步的。如果 UI 依赖持久化的值做首次渲染,可能会闪烁。解决方案:// 方法 1:用 onRehydrateStorage 设置 ready 标记 const [ready, setReady] = useState(false); // 在 onRehydrateStorage 的返回函数中 setReady(true) // 方法 2:skipHydration + 手动控制 // 在 App 顶层 useEffect 中调用 persist.rehydrate(), // 等 hasHydrated() 返回 true 后再渲染子组件
immer — 可变语法更新
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface Todo {
id: number;
text: string;
done: boolean;
}
interface TodoState {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
updateNested: (id: number, label: string) => void;
}
const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
// ✅ immer 允许直接 push(可变写法)
state.todos.push({ id: Date.now(), text, done: false });
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
}),
// 深层嵌套更新 —— immer 的优势场景
updateNested: (id, label) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo && todo.meta) {
todo.meta.labels.push(label); // 深层可变更新
}
}),
}))
);immer vs 不可变更新对比
// 不用 immer:深层更新非常繁琐
set((state) => ({
...state,
user: {
...state.user,
profile: {
...state.user.profile,
address: {
...state.user.profile.address,
city: 'Beijing',
},
},
},
}));
// 用 immer:直接赋值即可
set((state) => {
state.user.profile.address.city = 'Beijing';
});💡 最佳实践:当 Store 有深层嵌套对象或频繁的数组操作时,用
immer。如果状态结构扁平(1-2 层),手动不可变更新更简洁。
subscribeWithSelector — 精确订阅
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
const useStore = create(
subscribeWithSelector((set) => ({
count: 0,
name: 'Alice',
inc: () => set((s) => ({ count: s.count + 1 })),
}))
);
// ✅ 只监听 count 字段
useStore.subscribe(
(state) => state.count, // selector
(count, prevCount) => { // 变化时触发
console.log(`count: ${prevCount} → ${count}`);
},
{
equalityFn: (a, b) => a === b, // 自定义比较(默认 shallow)
fireImmediately: true, // 立即触发一次回调
}
);💡 最佳实践:只在需要
subscribe带选择器或在 vanilla 环境做字段级订阅时引入这个中间件。组件中不需要它——组件本身的选择器已经支持字段级订阅。
自定义中间件
import { create, StateCreator, StoreMutatorIdentifier } from 'zustand';
// 自定义中间件:每次 set 时打印日志
type Logger = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
f: StateCreator<T, Mps, Mcs>
) => StateCreator<T, Mps, Mcs>;
type LoggerImpl = <T>(
f: StateCreator<T, [], []>
) => StateCreator<T, [], []>;
const logger: LoggerImpl = (creator) => (set, get, api) => {
const enhancedSet: typeof set = (...args) => {
console.log(' 📝 set:', args[0]);
set(...args);
console.log(' 📦 new state:', get());
};
return creator(enhancedSet, get, api);
};
// 使用
const useStore = create(
logger((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
);实用自定义中间件示例
// 1. 重置中间件
import { StateCreator } from 'zustand';
interface ResetableStore {
reset: () => void;
}
const withReset = <T extends ResetableStore>(
initializer: StateCreator<T>
): StateCreator<T> =>
(set, get, api) => {
const initialState = initializer(set, get, api);
return {
...initialState,
reset: () => set(initializer(set, get, api) as Partial<T>, true),
};
};
// 2. Redux-like dispatch 中间件
const withDispatch = <T>(
creator: StateCreator<T>
): StateCreator<T & { dispatch: (action: any) => void }> =>
// 简化示意,实际场景中会按 action.type 分发
(set, get, api) => {
const store = creator(set, get, api);
return {
...store,
dispatch: (action) => {
// 根据 action.type 调用对应的 setter
console.log('dispatch:', action);
},
} as any;
};中间件组合实战
import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
const useStore = create<TodoState>()(
devtools( // 1. DevTools(最外层)
subscribeWithSelector( // 2. 选择器订阅
persist( // 3. 持久化
immer( // 4. 可变更新(最内层)
(set, get) => ({
todos: [],
addTodo: (text) =>
set((s) => {
s.todos.push({ id: Date.now(), text, done: false });
}),
}),
{ name: 'todos-storage' }
)
)
)
)
);💡 最佳实践:中间件组合时
immer在最内层(直接包裹 creator),devtools在最外层。这样persist持久化的是 Immer 处理后的不可变数据,devtools能看到所有中间件处理后的最终状态。