Zustand:极简状态管理的设计哲学
第12章:Zustand:极简状态管理的设计哲学
Zustand 的核心创见是一个看似简单的问题:为什么状态必须活在 React 里?
本章核心问题:Zustand 如何实现精准订阅?它与 Context 的性能差距有多大? 读完本章你将理解:
- Zustand 的 store 是普通的 JavaScript 闭包,完全独立于 React 存在
- Selector 是 Zustand 最重要的性能机制——通过 Object.is 比较实现精准订阅
- 中间件系统(immer、devtools、persist)展示了函数组合的设计美学
Level 1 · 你需要知道的(1-3年经验)
Store 即闭包,而非 Context
Zustand 的核心创见是一个看似简单的问题:为什么状态必须活在 React 里?
Redux 把状态放在 Store 对象中,通过 react-redux 的 Provider 注入 React 树。MobX 同样需要通过某种方式把 observable 状态注入组件。早期的 Context 方案更是把状态直接嵌入 React 的渲染机制。这些方案都有一个隐含假设:状态是 React 的东西。
Zustand 拒绝这个假设。它的 store 是一个普通的 JavaScript 闭包——一个模块级别的对象,完全独立于 React 存在。React 组件通过 useSyncExternalStore(React 18+)订阅这个外部存储,当状态改变时,只有真正订阅了变化的部分的组件才会重新渲染。
import { create } from 'zustand';
// Store 是一个普通函数调用的返回值,不依赖任何 React API
const useBearStore = create<{
bears: number;
increase: () => void;
reset: () => void;
}>()(set => ({
bears: 0,
increase: () => set(state => ({ bears: state.bears + 1 })),
reset: () => set({ bears: 0 }),
}));
// 使用时像普通 Hook 一样调用
function BearCounter() {
const bears = useBearStore(state => state.bears);
return <h1>{bears} bears around here...</h1>;
}
function Controls() {
const increase = useBearStore(state => state.increase);
return <button onClick={increase}>Add a bear</button>;
}
注意 useBearStore 的调用方式:传入一个 selector 函数,只订阅需要的那部分状态。Controls 组件只订阅 increase 函数——函数引用不变,所以 Controls 永远不会因为 bears 数量变化而重新渲染。这正是 Context 做不到的。
Selector:派生状态与性能
Selector 是 Zustand 最重要的性能机制。每次调用 useStore(selector),Zustand 都会用 Object.is 比较 selector 的返回值与上次的返回值,只有不相等时才触发重渲染。
// 粒度控制:只订阅需要的字段
const firstName = useUserStore(state => state.user?.firstName);
const isAdmin = useUserStore(state => state.user?.role === 'admin');
// 派生状态计算
const useCartStore = create<CartStore>()(set => ({
items: [] as CartItem[],
addItem: (item) => set(state => ({
items: [...state.items, item],
})),
removeItem: (id) => set(state => ({
items: state.items.filter(i => i.id !== id),
})),
}));
// 派生状态通过 selector 实现
function CartSummary() {
// 这个计算只在 items 数组变化时重新执行
const total = useCartStore(state =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
const itemCount = useCartStore(state => state.items.length);
return (
<div>
<span>{itemCount} items</span>
<span>Total: ${total.toFixed(2)}</span>
</div>
);
}
对于复杂的派生状态计算,建议用 useShallow 处理返回对象/数组的 selector:
import { useShallow } from 'zustand/react/shallow';
// 没有 useShallow:每次都返回新对象,导致无限重渲染
const { bears, increase } = useBearStore(state => ({
bears: state.bears,
increase: state.increase,
})); // 危险!每次渲染返回新对象,Object.is 始终为 false
// 有 useShallow:浅比较对象,只在内容变化时重渲染
const { bears, increase } = useBearStore(
useShallow(state => ({ bears: state.bears, increase: state.increase }))
);
中间件系统:immer、devtools、persist
Zustand 的中间件是真正展示其设计美学的地方。中间件是函数组合,不是配置对象——这意味着你可以任意组合,且每个中间件的职责清晰。
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { devtools } from 'zustand/middleware';
import { persist } from 'zustand/middleware';
// 组合多个中间件
const useTaskStore = create<TaskStore>()(
devtools(
persist(
immer((set) => ({
tasks: [] as Task[],
addTask: (task: Task) => set(state => {
// immer 允许直接 mutate,内部用 Proxy 追踪变更
state.tasks.push(task);
}),
toggleTask: (id: string) => set(state => {
const task = state.tasks.find(t => t.id === id);
if (task) task.completed = !task.completed;
}),
deleteTask: (id: string) => set(state => {
state.tasks = state.tasks.filter(t => t.id !== id);
}),
})),
{
name: 'task-storage', // localStorage key
// 只持久化部分状态
partialize: (state) => ({ tasks: state.tasks }),
}
),
{ name: 'TaskStore' } // DevTools 中显示的名称
)
);
immer 中间件:告别手动不可变更新
没有 immer 时,更新嵌套状态极其繁琐:
// 没有 immer:手动扩展每一层
set(state => ({
user: {
...state.user,
profile: {
...state.user.profile,
address: {
...state.user.profile.address,
city: 'Shanghai',
},
},
},
}));
// 有 immer:直接赋值,Proxy 追踪变更并生成不可变更新
set(state => {
state.user.profile.address.city = 'Shanghai';
});
immer 使用 Proxy 追踪对 draft 对象的所有修改,在 producer 函数执行结束后生成一个新的不可变对象。零运行时 overhead 的心智模型,极大降低了复杂状态管理的出错率。
persist 中间件:状态持久化
import { persist, createJSONStorage } from 'zustand/middleware';
const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'light' as 'light' | 'dark',
language: 'zh-CN',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: 'user-settings',
// 默认使用 localStorage,可以替换为 sessionStorage 或自定义
storage: createJSONStorage(() => sessionStorage),
// 版本迁移:当 store 结构改变时自动迁移旧数据
version: 2,
migrate: (persistedState: unknown, version: number) => {
if (version === 1) {
// 将旧格式迁移到新格式
const old = persistedState as { darkMode: boolean };
return { theme: old.darkMode ? 'dark' : 'light', language: 'zh-CN' };
}
return persistedState as SettingsStore;
},
}
)
);
Slices 模式:大型应用的状态拆分
当应用规模增长,把所有状态放在一个 store 里会变得难以维护。Zustand 的 "slices" 模式解决了这个问题,同时保留了访问跨域状态的能力:
// authSlice.ts
export interface AuthSlice {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
export const createAuthSlice = (
set: SetState<RootStore>,
get: GetState<RootStore>
): AuthSlice => ({
user: null,
login: async (credentials) => {
const user = await authAPI.login(credentials);
set({ user });
},
logout: () => {
set({ user: null });
// 注意:通过 get() 可以访问其他 slice 的状态和方法
get().clearCart(); // 清空购物车(来自 cartSlice)
},
});
// cartSlice.ts
export interface CartSlice {
cart: CartItem[];
addToCart: (item: CartItem) => void;
clearCart: () => void;
}
export const createCartSlice = (
set: SetState<RootStore>
): CartSlice => ({
cart: [],
addToCart: (item) => set(state => ({ cart: [...state.cart, item] })),
clearCart: () => set({ cart: [] }),
});
// store.ts:组合所有 slice
type RootStore = AuthSlice & CartSlice;
const useStore = create<RootStore>()((...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args),
}));
// 按需导出特定 slice 的 selector
export const useAuth = () => useStore(useShallow(state => ({
user: state.user,
login: state.login,
logout: state.logout,
})));
export const useCart = () => useStore(useShallow(state => ({
cart: state.cart,
addToCart: state.addToCart,
})));
Level 2 · 它是怎么运行的(3-5年经验)
Zustand 内部实现:useSyncExternalStore
理解 Zustand 的性能优势,需要深入它的内部实现。Zustand 的核心是 createStore 函数,它创建一个符合"外部存储"(external store)接口的对象:
// 简化版的 Zustand 核心实现(概念演示)
function createStore<T>(initializer: StateCreator<T>) {
let state: T;
const listeners = new Set<() => void>();
const setState = (partial: Partial<T> | ((state: T) => Partial<T>)) => {
const nextState = typeof partial === 'function'
? { ...state, ...partial(state) }
: { ...state, ...partial };
if (!Object.is(state, nextState)) {
state = nextState;
// 通知所有订阅者:状态改变了
listeners.forEach(listener => listener());
}
};
const getState = () => state;
const subscribe = (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener); // 返回取消订阅函数
};
state = initializer(setState, getState, { setState, getState, subscribe });
return { setState, getState, subscribe };
}
// React Hook 层:使用 useSyncExternalStore 桥接外部存储与 React
function useStore<T, S>(store: Store<T>, selector: (state: T) => S): S {
return useSyncExternalStore(
store.subscribe, // 订阅函数
() => selector(store.getState()), // 获取当前值
() => selector(store.getState()), // 服务端渲染的初始值
);
}
useSyncExternalStore 是 React 18 专门为状态管理库提供的 API。它解决了 React Concurrent Mode 下的 "tearing"(撕裂)问题——即在同一次渲染中,组件可能读取到不同版本的外部状态。useSyncExternalStore 保证了在并发渲染过程中,订阅同一外部存储的所有组件看到的状态是一致的。
Level 3 · 规范怎么定义的(资深)
create() 的类型系统与 API 设计
Zustand 的 TypeScript 集成是精心设计的,但有一个让初学者困惑的地方:create() 需要"柯里化调用":
// 正确的 TypeScript 写法(注意双括号)
const useStore = create<StoreState>()(/* initializer */);
// 这是 Zustand 绕过 TypeScript 类型推断限制的解决方案:
// TypeScript 不支持部分类型参数应用(partial type argument application)
// 柯里化让 TS 能先接收显式类型参数,再接收运行时参数
// 完整的 Store 类型定义
interface UserStore {
user: User | null;
isLoggedIn: boolean;
setUser: (user: User) => void;
logout: () => void;
}
const useUserStore = create<UserStore>()(set => ({
user: null,
isLoggedIn: false,
setUser: (user) => set({ user, isLoggedIn: true }),
logout: () => set({ user: null, isLoggedIn: false }),
}));
与 Context 的性能对比
回到上一章的基准场景,用代码完整展示差距:
// 场景:50 个子组件,共享计数器,每 100ms 更新一次
// Context 方案
function runContextBenchmark() {
// 所有 50 个消费者在每次更新时都重新渲染
// React DevTools Profiler: 每次更新约 50 次渲染,总计 ~12ms
}
// Zustand 方案
const useCountStore = create<{ count: number; noise: string }>()(set => ({
count: 0,
noise: 'irrelevant',
increment: () => set(state => ({ count: state.count + 1 })),
updateNoise: () => set({ noise: Math.random().toString() }),
}));
// 只订阅 count 的组件
function Counter() {
const count = useCountStore(state => state.count); // 精准订阅
return <div>{count}</div>;
}
// updateNoise 不会导致 Counter 重渲染——Object.is(oldCount, newCount) 为 true
// React DevTools Profiler: 每次 count 更新约 1 次渲染(该组件),总计 ~2ms
Zustand 在大型应用中的性能优势不仅来自于减少不必要的重渲染,还来自于它将状态更新决策从 React 的渲染调度器移到了纯 JavaScript 层——这让状态读写的性能接近普通对象访问的速度。
Zustand 的设计哲学证明:最好的抽象往往不是功能最多的,而是概念最少、但每个概念都恰到好处的。一个 store,一个 Hook,一个 selector——就这些。
Level 4 · 边界与陷阱(所有人)
生产环境常见问题
在实际项目中,本章涵盖的概念最常见的问题包括:
-
忽视边界条件:在正常路径下工作正常的代码,在异常路径(网络失败、用户快速操作、组件卸载)下可能产生 bug。始终考虑清理和取消逻辑。
-
过早优化:在没有测量的情况下添加优化代码,增加了复杂度但不一定提升性能。先用 Profiler 确认问题存在,再实施优化。
-
文档与实际行为的差异:React 的行为在不同版本间可能有微妙差异,尤其是并发模式相关的特性。始终以实际测试结果为准。