前端状态管理:Zustand,一个轻量的全局状态方案
Zustand 这几年在 React 生态里越来越常见,原因很简单:API 小、心智负担低、和 Hooks 契合得比较自然。
如果项目里不想上一个「全家桶」式的状态管理库,但又希望有个靠谱的全局状态方案,可以把它当成首选之一。
基本使用方式:一个 store,一个 hook
Zustand 的核心只有两件事:
- 用
create定义一个 store; - 在组件里用返回的 hook 读写状态。
示例:
1import { create } from 'zustand';
2
3type BearState = {
4 count: number;
5 increase: () => void;
6};
7
8const useBearStore = create<BearState>((set) => ({
9 count: 0,
10 increase: () => set((state) => ({ count: state.count + 1 })),
11}));
12
13function BearCounter() {
14 const count = useBearStore((state) => state.count);
15 const increase = useBearStore((state) => state.increase);
16
17 return (
18 <div>
19 <span>{count}</span>
20 <button onClick={increase}>+1</button>
21 </div>
22 );
23}
几个点:
- store 不是通过
<Provider>注入,而是一个普通的 hook,随用随引; set支持传对象或函数,函数里可以拿到前一个 state;- 组件里按「选择器」拿自己关心的那一小块状态。
选择器与避免多余渲染
Zustand 鼓励用选择器精确订阅状态的一部分:
1const count = useBearStore((state) => state.count);
默认情况下,Zustand 会用 Object.is 比较选择器的返回结果,只有变化时才触发重渲染。
如果需要更细粒度控制,可以配合 shallow:
1import { shallow } from 'zustand/shallow';
2
3const { x, y } = useStore(
4 (state) => ({ x: state.x, y: state.y }),
5 shallow
6);
这在订阅多个字段但又希望避免「任一字段变化就导致整块对象被认为不同」的时候比较有用。
切片(slice)和多模块拆分
项目一大,一锅端的 store 很快就会变得难以维护。
常见的做法是按领域拆成多个 slice,再在 create 时组合起来:
1type UserSlice = {
2 user: { id: string; name: string } | null;
3 setUser: (user: UserSlice['user']) => void;
4};
5
6type TodoSlice = {
7 todos: string[];
8 addTodo: (text: string) => void;
9};
10
11type StoreState = UserSlice & TodoSlice;
12
13const createUserSlice = (set: any): UserSlice => ({
14 user: null,
15 setUser: (user) => set({ user }),
16});
17
18const createTodoSlice = (set: any): TodoSlice => ({
19 todos: [],
20 addTodo: (text) => set((state: StoreState) => ({
21 todos: [...state.todos, text],
22 })),
23});
24
25const useStore = create<StoreState>((set) => ({
26 ...createUserSlice(set),
27 ...createTodoSlice(set),
28}));
这样每个 slice 专注一块领域逻辑,需要时在组件中用同一个 useStore hook 通过选择器拿对应部分。
中间件:持久化、调试和重置
Zustand 的「中间件」是一大卖点,用来给 store 增加能力而不改动核心逻辑。
比较常见的几个:
persist:把状态持久化到 localStorage 等;devtools:接入 Redux DevTools;immer:用 immer 的方式写状态更新。
以持久化为例:
1import { create } from 'zustand';
2import { persist } from 'zustand/middleware';
3
4type SettingsState = {
5 theme: 'light' | 'dark';
6 setTheme: (theme: SettingsState['theme']) => void;
7};
8
9const useSettingsStore = create<SettingsState>()(
10 persist(
11 (set) => ({
12 theme: 'light',
13 setTheme: (theme) => set({ theme }),
14 }),
15 {
16 name: 'app-settings', // localStorage key
17 }
18 )
19);
只在意状态本身的逻辑可以保持很干净,持久化细节交给中间件处理。
与异步请求配合
Zustand 本身不关心异步,你可以在 action 里直接写 async,也可以把异步放在外部逻辑里:
1type UserState = {
2 user: { id: string; name: string } | null;
3 loading: boolean;
4 error: string | null;
5 fetchUser: (id: string) => Promise<void>;
6};
7
8const useUserStore = create<UserState>((set) => ({
9 user: null,
10 loading: false,
11 error: null,
12 async fetchUser(id) {
13 set({ loading: true, error: null });
14 try {
15 const res = await fetch(`/api/users/${id}`);
16 const data = await res.json();
17 set({ user: data, loading: false });
18 } catch (e: any) {
19 set({ error: e?.message ?? '请求失败', loading: false });
20 }
21 },
22}));
组件里只关心状态形态,不需要知道请求的细节:
1const { user, loading, error, fetchUser } = useUserStore((state) => state);
如果项目已经用了 React Query / SWR 这类数据请求库,Zustand 更适合作为「UI 状态 / 跨组件控制状态」的容器,例如当前选中项、侧边栏开关、临时过滤条件等。
和 Context / Redux 的对比
简单对比一下几个常见选择:
React Context + useReducer
- 优点:不引入额外依赖,类型推断友好;
- 缺点:一旦 state 变大、更新频繁,很容易出现整棵子树重渲染的问题,需要额外拆分优化。
Redux / Redux Toolkit
- 优点:生态成熟、调试工具完善、约束明确;
- 缺点:心智模型相对重,不是所有项目都需要整套架子。
Zustand
- 优点:体积小、API 简洁、默认就支持选择器和细粒度订阅;
- 缺点:约束相对少,项目大了之后需要团队约定好基本用法(如何拆 slice、命名、在哪里写异步等)。
对于中小体量的应用,或者前后端都由一两个核心开发维护的项目,Zustand 的成本和收益比通常是比较合适的。
在项目里落地时可以注意的点
- 明确好「哪些状态放在 Zustand」「哪些直接用组件内 state」,避免什么都往全局里堆;
- store 拆分按领域来,而不是按页面来,这样更利于复用;
- 给 store 的 action 起清晰的名字,和领域语言贴近,而不是一堆
setXxx; - 搭配 TypeScript,尽量把 store 类型定义成独立的
State/Action接口,出问题时更容易排查。
整体来说,Zustand 更像是一块可塑性比较强的乐高砖块:
愿意做一点结构设计,就能搭出一套干净的状态层;不想上太重方案时,它也足够轻。