Zustand 深入
极简状态管理:基础使用、中间件体系、核心原理与源码解析
概述
Zustand(德语"状态")是由 Pmndrs(Poimandres)团队开发的轻量级状态管理库,核心代码仅约 40 行。它的设计哲学是:用最少的 API 覆盖最常见的状态管理需求。
一、优势与劣势
优势
- 极简 API:
create一个函数就完成 store 定义,无 Provider、无 Action Type、无 Reducer - 极小体积:gzip 后约 1KB,几乎零成本引入
- 无 Provider:store 是模块级单例,不污染组件树,也可在 React 外使用
- 选择器订阅:通过 selector 函数实现细粒度订阅,只有选中的值变化才触发重渲染
- 中间件生态:persist(持久化)、devtools(调试)、immer(可变风格)、subscribeWithSelector 等
- TypeScript 友好:类型推断开箱即用
- 异步原生支持:action 中直接
async/await,无需额外中间件(对比 Redux Thunk/Saga)
劣势
- 无内置派生状态:派生计算需要手动在 selector 或组件中实现(对比 Jotai 的 derived atom)
- 单 store 模式:大型应用如果所有状态放一个 store,组织性不如 Redux Toolkit 的 slice 划分清晰
- selector 引用陷阱:返回新对象/数组的 selector 需要配合
shallow比较,否则每次都重渲染 - 调试能力弱于 Redux:虽然支持 DevTools,但缺少 action 日志和时间旅行的完整体验
二、基础使用
安装
bash
npm install zustand创建 Store
jsx
import { create } from 'zustand';
const useStore = create((set, get) => ({
// 状态
count: 0,
user: null,
todos: [],
// 同步 action
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// 异步 action
fetchUser: async (id) => {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
set({ user });
},
// 使用 get() 读取当前状态
addTodo: (text) => {
const todos = get().todos;
set({ todos: [...todos, { id: Date.now(), text, done: false }] });
},
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
),
})),
}));在组件中使用
jsx
// 选择器:只订阅需要的状态片段
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
// count 变化时重渲染,todos 变化时不重渲染
return <button onClick={increment}>{count}</button>;
}
function UserProfile() {
const user = useStore((state) => state.user);
const fetchUser = useStore((state) => state.fetchUser);
useEffect(() => { fetchUser(1); }, [fetchUser]);
if (!user) return <Skeleton />;
return <div>{user.name}</div>;
}
function TodoList() {
const todos = useStore((state) => state.todos);
const toggleTodo = useStore((state) => state.toggleTodo);
return (
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</li>
))}
</ul>
);
}Selector 与浅比较
jsx
import { useShallow } from 'zustand/react/shallow';
// ❌ 每次返回新对象 → 每次都重渲染
const { count, name } = useStore((state) => ({
count: state.count,
name: state.name,
}));
// ✅ useShallow 进行浅比较 → 只有值真正变化才重渲染
const { count, name } = useStore(
useShallow((state) => ({ count: state.count, name: state.name }))
);在 React 外使用
javascript
// 在非 React 代码中(如路由守卫、工具函数)
const state = useStore.getState();
console.log(state.count);
useStore.setState({ count: 99 });
// 订阅变化
const unsub = useStore.subscribe((state) => {
console.log('状态变化:', state);
});三、中间件
persist —— 持久化
jsx
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{
name: 'app-settings', // localStorage key
storage: createJSONStorage(() => localStorage), // 默认 localStorage
partialize: (state) => ({ theme: state.theme }), // 只持久化部分状态
version: 1, // 版本号(迁移用)
migrate: (persisted, version) => { // 版本迁移
if (version === 0) {
persisted.theme = persisted.theme || 'light';
}
return persisted;
},
}
)
);devtools —— Redux DevTools
jsx
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools(
(set) => ({
count: 0,
// 第三个参数给 action 命名(在 DevTools 中显示)
increment: () => set(
(state) => ({ count: state.count + 1 }),
false,
'increment'
),
}),
{ name: 'CounterStore', enabled: process.env.NODE_ENV === 'development' }
)
);immer —— 可变风格更新
jsx
import { immer } from 'zustand/middleware/immer';
const useStore = create(
immer((set) => ({
todos: [],
addTodo: (text) => set((state) => {
state.todos.push({ id: Date.now(), text, done: false }); // 直接 push
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done; // 直接修改
}),
}))
);中间件组合
jsx
const useStore = create(
devtools(
persist(
immer((set) => ({
// ...状态和 action
})),
{ name: 'my-storage' }
),
{ name: 'MyStore' }
)
);
// 顺序:外层到内层 → devtools → persist → immer → 核心逻辑四、模式与最佳实践
Slice 模式(大型应用拆分)
typescript
// slices/userSlice.ts
interface UserSlice {
user: User | null;
fetchUser: (id: string) => Promise<void>;
}
const createUserSlice = (set, get): UserSlice => ({
user: null,
fetchUser: async (id) => {
const user = await fetchUserAPI(id);
set({ user });
},
});
// slices/todoSlice.ts
interface TodoSlice {
todos: Todo[];
addTodo: (text: string) => void;
}
const createTodoSlice = (set, get): TodoSlice => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, done: false }],
})),
});
// store.ts — 合并 slice
const useStore = create<UserSlice & TodoSlice>()((...args) => ({
...createUserSlice(...args),
...createTodoSlice(...args),
}));五、核心原理
源码解析(简化)
javascript
// Zustand 核心实现只有约 40 行
function createStore(createState) {
let state;
const listeners = new Set();
const getState = () => state;
const setState = (partial, replace) => {
const nextState = typeof partial === 'function' ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state = replace ? nextState : { ...state, ...nextState };
listeners.forEach((listener) => listener(state, previousState));
}
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const api = { getState, setState, subscribe };
state = createState(setState, getState, api);
return api;
}与 React 的连接
javascript
// create() 返回的 useStore Hook 基于 useSyncExternalStore
import { useSyncExternalStore } from 'react';
function useStore(selector, equalityFn = Object.is) {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
() => selector(store.getServerState?.() ?? store.getState()) // SSR
);
}关键设计
- 发布-订阅模式:
subscribe注册监听器,setState触发通知 - useSyncExternalStore:React 18 官方 API,保证并发模式下的数据一致性(无 tearing)
- selector 决定重渲染:每次 state 变化后,React 用 selector 取值并比较,值没变则跳过渲染
- 闭包单例:store 存在模块作用域闭包中,不依赖 Context / Provider
面试高频题
Q: Zustand 为什么不需要 Provider?
Zustand 的 store 是模块级单例,状态存储在 createStore 的闭包中。组件通过 useSyncExternalStore 直接订阅这个外部 store,不需要 React Context 的传递链。好处是更简洁,而且可以在 React 组件外(如路由守卫、工具函数)直接读写状态。
Q: Zustand 的 selector 是怎么避免不必要重渲染的?
useSyncExternalStore 在 store 变化时,调用 selector 取新值,与上一次的值进行 Object.is 比较。如果相等则跳过重渲染。所以:
- 选择原始值(
state.count)天然稳定 - 选择新对象需要配合
useShallow或自定义equalityFn
Q: Zustand 和 Redux 的核心区别?
- API 复杂度:Zustand 无 action type / reducer / dispatch,一个
create搞定 - Provider:Redux 必须 Provider 包裹,Zustand 不需要
- 异步:Zustand 直接 async/await,Redux 需要 thunk/saga 中间件
- 体积:Zustand ~1KB vs Redux Toolkit ~11KB
- 适合场景:Zustand 适合中小项目快速上手,Redux 适合大型团队严格规范