Skip to content
中间件

中间件

中间件是 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 能看到所有中间件处理后的最终状态。