Skip to content
高级模式

高级模式

本章涵盖 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. persiststorage 选项增强

// 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/vanillacreateStore 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 实例。适合需要多实例的场景(如多个独立的表单、多个图表)。