核心概念
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。
三剑客:set、get、subscribe
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 类型 | 泛型参数,自动推导 |