Skip to content
第 17 章 前端 ⏱ 13 分钟阅读

第 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 ​

维度ZustandRedux Toolkit
体积1KB10KB+
样板代码极少中等
学习成本极低中等
DevTools支持强大
适用中小型大型团队

十、本章小结 ​

要点关键
安装npm i zustand
创建create<State>((set) => ({...}))
selector(s) => s.field,避免取整个 store
shallow对象 selector 必加 shallow
异步直接在 action 里 await
持久化persist() 中间件
外部读useStore.getState()

动手练习 ​

  1. 计数器 store:创建 useCounter,支持 +、-、reset
  2. Todo store:完整 CRUD + 持久化到 localStorage
  3. 用户 store:异步 fetch,loading/error 状态
  4. 找坑:以下代码哪里错?
    tsx
    const store = useBearStore();

下一章:第 18 章:组件库 →

本站基于 VitePress 构建 · 由 StackHub 团队维护