第 17 章:状态管理
学习目标
- 理解为什么需要状态管理库
- 掌握 Zustand 的核心 API
- 用 selector 优化重渲染
- 避开状态管理的 4 个常见坑
一、为什么不用 Context 就够了?
Context 在大型应用的问题:
- 任何 state 变化全量通知所有消费者
- 没有 selector(选择性订阅)
- 没有 devtools 时间旅行
中型以上项目用 Zustand / Redux Toolkit。
二、Zustand 安装
bash
npm install zustand三、第一个 Store
tsx
import { create } from 'zustand';
interface BearState {
bears: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: () => set((s) => ({ bears: s.bears + 1 })),
decrease: () => set((s) => ({ bears: s.bears - 1 })),
reset: () => set({ bears: 0 }),
}));
// 组件使用
function Bear() {
const bears = useBearStore((s) => s.bears); // �️ 用 selector
const increase = useBearStore((s) => s.increase);
return <button onClick={increase}>{bears} bears</button>;
}⚠️ 坑 1:必须用 selector 取值,不要
const s = useBearStore()(返回整个 store,任何字段变化都重渲染)。
四、Action 集中管理
tsx
interface TodoState {
list: Todo[];
add: (text: string) => void;
toggle: (id: number) => void;
remove: (id: number) => void;
}
const useTodoStore = create<TodoState>((set) => ({
list: [],
add: (text) => set((s) => ({
list: [...s.list, { id: Date.now(), text, done: false }],
})),
toggle: (id) => set((s) => ({
list: s.list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
})),
remove: (id) => set((s) => ({
list: s.list.filter((t) => t.id !== id),
})),
}));五、selector 与浅比较
tsx
// ✅ 单值 selector:自动浅比较
const bears = useBearStore((s) => s.bears);
// ⚠️ 返回新对象时,默认 Object.is 比较,引用变了就重渲染
const { bears, increase } = useBearStore((s) => ({ bears: s.bears, increase: s.increase }));
// ✅ 用 shallow 解决(对象属性浅比较)
import { shallow } from 'zustand/shallow';
const { bears, increase } = useBearStore(
(s) => ({ bears: s.bears, increase: s.increase }),
shallow,
);⚠️ 坑 2:返回对象的 selector 必须用
shallow,否则每次渲染都重渲染。
六、异步 Action
tsx
interface UserState {
user: User | null;
loading: boolean;
fetchUser: (id: number) => Promise<void>;
}
const useUserStore = create<UserState>((set) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true });
const r = await fetch(`/api/users/${id}`);
const user = await r.json();
set({ user, loading: false });
},
}));
// 组件
function UserDetail({ id }: { id: number }) {
const { user, loading, fetchUser } = useUserStore(
(s) => ({ user: s.user, loading: s.loading, fetchUser: s.fetchUser }),
shallow,
);
useEffect(() => { fetchUser(id); }, [id]);
if (loading) return <Spinner />;
return <div>{user?.name}</div>;
}⚠️ 坑 3:
fetchUser直接放进 selector 可能无限重渲染,要么用shallow,要么用 store action 而非 selector。
七、持久化(中间件)
tsx
import { persist } from 'zustand/middleware';
const useUserStore = create<UserState>()(
persist(
(set) => ({
token: null,
login: (token: string) => set({ token }),
logout: () => set({ token: null }),
}),
{ name: 'user-store' }, // localStorage key
),
);
// 配置:仅持久化部分字段
persist(
(set) => ({ ... }),
{
name: 'app',
partialize: (state) => ({ token: state.token }), // ⚠️ 只存 token
},
);八、跨组件订阅(无 React)
tsx
// 在组件外也能读
const token = useUserStore.getState().token;
// 设置
useUserStore.setState({ token: 'xxx' });
// 订阅
const unsub = useUserStore.subscribe((state, prev) => {
if (state.token !== prev.token) console.log('token 变化');
});九、Zustand vs Redux Toolkit
| 维度 | Zustand | Redux Toolkit |
|---|---|---|
| 体积 | 1KB | 10KB+ |
| 样板代码 | 极少 | 中等 |
| 学习成本 | 极低 | 中等 |
| DevTools | 支持 | 强大 |
| 适用 | 中小型 | 大型团队 |
十、本章小结
| 要点 | 关键 |
|---|---|
| 安装 | npm i zustand |
| 创建 | create<State>((set) => ({...})) |
| selector | (s) => s.field,避免取整个 store |
| shallow | 对象 selector 必加 shallow |
| 异步 | 直接在 action 里 await |
| 持久化 | persist() 中间件 |
| 外部读 | useStore.getState() |
动手练习
- 计数器 store:创建
useCounter,支持+、-、reset - Todo store:完整 CRUD + 持久化到 localStorage
- 用户 store:异步 fetch,loading/error 状态
- 找坑:以下代码哪里错?tsx
const store = useBearStore();
下一章:第 18 章:组件库 →