Skip to content
核心概念

核心概念

Zustand 的核心概念极其简洁:一个 Store 就是一个包含状态和操作的 JavaScript 对象,通过 create 转化为一个 React Hook。


核心三要素

要素 说明 代码中的体现
State 存储的数据 bears: 0
Actions 修改 state 的函数 increase: () => set(...)
Selector 从 state 中选取特定字段 useStore(s => s.bears)
import { create } from 'zustand';

const useStore = create((set, get) => ({
  // ─── State ───
  count: 0,
  user: null,

  // ─── Actions ───
  increment: () => set((state) => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
  // get() 可以读取最新状态,无需放入依赖
  log: () => console.log(`count: ${get().count}`),
}));

create 函数

create 是 Zustand 的唯一入口。它接收一个 creator 函数,返回一个 React Hook

const useStore = create<T>()((set, get, api) => ({
  // state + actions
}));

create 的三种写法

// 写法一:最简形式(无类型参数)
const useStore = create((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}));

// 写法二:带类型参数(推荐)
interface Store {
  count: number;
  inc: () => void;
}
const useStore = create<Store>()((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}));

// 写法三:带中间件 + 类型参数(注意双重括号)
const useStore = create<Store>()(
  devtools(
    persist((set) => ({
      count: 0,
      inc: () => set((s) => ({ count: s.count + 1 })),
    }), { name: 'my-store' })
  )
);

🚨 陷阱:使用 TypeScript 中间件时,create<T>()(...)双重括号很容易遗漏。第一层 () 是泛型实例化,第二层 (...) 才是传入 creator。


三剑客:setgetsubscribe

Zustand Store 内部有三个核心方法:

set — 修改状态

const useStore = create((set) => ({
  count: 0,

  // 方式 1:传入新状态对象(浅合并)
  setCount: (n: number) => set({ count: n }),
  // 内部执行:Object.assign(state, { count: n })

  // 方式 2:传入函数,基于当前 state 计算(推荐)
  increment: () => set((state) => ({ count: state.count + 1 })),

  // 方式 3:第二个参数 replace = true,替换整个 state(不合并)
  reset: () => set({ count: 0 }, true),

  // 方式 4:第二个参数是 action name(用于 DevTools)
  increment: () =>
    set((state) => ({ count: state.count + 1 }), false, 'increment'),
}));
set 参数 类型 说明
第 1 参数 Partial<T> | (state: T) => Partial<T> 新状态或更新函数
第 2 参数 boolean true 时替换整个 state(不浅合并),默认 false
第 3 参数 string Action 名称,在 DevTools 中显示

🔬 深入原理:Zustand 默认使用浅合并(shallow merge)——set({ a: 1 }) 只会更新 a,不会影响其他顶层字段。这与 React 的 setState 不同(setState 也是浅合并),但与 Redux reducer 行为一致。浅合并即 Object.assign(oldState, newPartial)

get — 读取最新状态

const useStore = create((set, get) => ({
  count: 0,

  // 在 action 中读取最新状态(无需将 count 放入参数)
  log: () => {
    const { count } = get();
    console.log(`Current count: ${count}`);
  },

  // 在条件判断中使用
  doubleIfLessThan10: () => {
    if (get().count < 10) {
      set({ count: get().count * 2 });
    }
  },
}));

get() 永远返回最新的状态——不受闭包影响。这在定时器、事件回调等场景中非常有用。

subscribe — 监听状态变化

const useStore = create((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}));

// 订阅状态变化(监听整个 store 或某个字段)
const unsub = useStore.subscribe(
  (state, prevState) => {
    console.log('count changed:', prevState.count, '→', state.count);
  }
);

// 取消订阅
unsub();

subscribe 还可以只监听指定字段:

// v4.3+ 配合 subscribeWithSelector 中间件
useStore.subscribe(
  (state) => state.count,
  (count, prevCount) => {
    console.log(`count: ${prevCount}${count}`);
  }
);

Vanilla Store vs React Hook

create 返回的对象既是 React Hook,也是一个 vanilla store:

const useBearStore = create((set) => ({
  bears: 0,
  increase: () => set((s) => ({ bears: s.bears + 1 })),
}));

// React Hook 用法
function Component() {
  const bears = useBearStore((s) => s.bears);
  // ...
}

// Vanilla API 用法(可在任何地方使用)
const bears = useBearStore.getState().bears;       // 读
useBearStore.setState({ bears: 10 });              // 写
useBearStore.getState().increase();                // 调用 action
const unsub = useBearStore.subscribe(console.log); // 订阅
useBearStore.destroy();                            // 销毁 store(移除所有 listener)
API 说明 适用场景
useStore(selector) React Hook,订阅状态 组件内
useStore.getState() 同步读取当前状态 任何地方
useStore.setState(partial) 直接修改状态(绕过 action) 外部同步、测试
useStore.subscribe(fn) 注册监听器,返回取消函数 日志、副作用
useStore.destroy() 移除所有监听器 清理、测试

💡 最佳实践:在 React 组件外部(WebSocket 回调、定时器、测试等)使用 getState() / setState() / subscribe(),组件内部始终使用 Hook 选择器。


不可变更新 vs 可变更新

Zustand 默认需要不可变更新(和 React 一致):

// ✅ 不可变更新 —— 正确
set((state) => ({
  todos: [...state.todos, newTodo],           // 展开数组
  user: { ...state.user, name: 'new' },       // 展开对象
  nested: {
    ...state.nested,
    deep: { ...state.nested.deep, value: 1 }, // 深层嵌套
  },
}));

// ❌ 可变更新 —— 不会触发重渲染!
set((state) => {
  state.todos.push(newTodo);  // 直接修改了旧引用
  return state;               // 返回同一个引用,Zustand 认为没变化
});

🚨 陷阱:Zustand 使用 Object.is 做引用比较。如果返回的对象引用和旧的一样,不会通知订阅者。直接修改 state 不会触发重渲染。

搭配 Immer 中间件实现可变更新

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer((set) => ({
    todos: [],
    addTodo: (text) =>
      set((state) => {
        // ✅ Immer 中间件允许"可变"写法
        state.todos.push({ id: Date.now(), text, done: false });
      }),
  }))
);

发布-订阅模式(原理)

                  create()
                     │
                     ▼
    ┌──────────────────────────────────┐
    │         Zustand Store             │
    │                                   │
    │  state ←── set() 修改             │
    │    │                              │
    │    ├── listener_1 (组件 A)        │
    │    ├── listener_2 (组件 B)        │
    │    └── listener_3 (subscribe)     │
    │                                   │
    │  set() 执行后遍历所有 listener    │
    │  通知它们重新读取状态              │
    └──────────────────────────────────┘

🔬 深入原理:Zustand 内部维护一个 Set<Listener>set() 被调用后会浅合并新状态,然后遍历所有 listener 调用它们。每个在组件中使用的 useStore(selector) 就是一个 listener——当 selector 返回的值与上次不同(Object.is 比较),组件就会重渲染。


与 React Context 的本质区别

维度 React Context Zustand
更新粒度 整个 Context value 变化 → 所有消费者重渲染 selector 返回的单个字段变化 → 仅该字段的消费者重渲染
Provider 必须包裹 不需要
组件外使用 ❌ 不能(除非传 dispatch) ✅ getState() / setState()
选择器 ❌ 没有(需手动拆分 Context 或用 use-context-selector) ✅ 内置,精细到单个字段
TypeScript 需显式声明 Context 类型 泛型参数,自动推导