Chapter 05

状态管理:Context 与 Zustand

从 Props Drilling 到全局状态——选择合适的状态管理方案,让数据流清晰可控

1. 何时需要全局状态

在 React 中,状态管理遵循单向数据流:数据从父组件向子组件流动(通过 Props),子组件通过回调函数通知父组件更改状态。这在组件层级较浅时运作良好,但随着应用变大,会出现问题。

Props Drilling 问题

假设有这样的组件树,App 中的 user 状态需要在深层的 Avatar 组件中使用:

// Props Drilling —— user 被迫经过所有中间组件
function App() {
  const [user, setUser] = useState(currentUser);
  return <Layout user={user} />;   // Layout 不需要 user,但要传下去
}

function Layout({ user }: Props) {
  return <Sidebar user={user} />;  // Sidebar 也不需要,继续传
}

function Sidebar({ user }: Props) {
  return <UserMenu user={user} />; // 还是要传
}

function UserMenu({ user }: Props) {
  return <Avatar user={user} />;  // 终于到达目的地
}

function Avatar({ user }: Props) {
  return <img src={user.avatar} />; // 真正使用 user 的地方
}

中间的 LayoutSidebarUserMenu 根本不需要 user,却被迫接受并传递它。当 User 类型改变时,所有中间组件都要修改——这就是 Props Drilling(属性穿透)问题。

状态提升的极限

当多个不相关的组件需要共享同一状态时,需要把状态提升到它们最近的公共祖先。如果这个祖先是顶层 App,每次状态变化都会导致整个应用重渲染,既低效又难以维护。

ℹ️

何时考虑全局状态:当同一状态需要被相距较远的多个组件访问或修改,Props Drilling 层级超过 3-4 层,或者需要跨页面持久化状态时。

2. Context API 详解

React 内置的 Context API 让你绕过 Props,直接把数据注入任意深度的子组件。

createContext + Provider + useContext 三件套

import { createContext, useContext, useState, useCallback } from 'react';

// 步骤 1:创建 Context,指定类型和默认值
interface AuthContextType {
  user: User | null;
  isLoggedIn: boolean;
  login(credentials: Credentials): Promise<void>;
  logout(): void;
}

const AuthContext = createContext<AuthContextType | null>(null);

// 步骤 2:Provider 组件 —— 封装状态逻辑,提供给子树
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = useCallback(async (credentials: Credentials) => {
    const u = await authService.login(credentials);
    setUser(u);
  }, []);

  const logout = useCallback(() => setUser(null), []);

  return (
    <AuthContext.Provider value={{ user, isLoggedIn: !!user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// 步骤 3:自定义 Hook 封装,避免每次都写 useContext
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth 必须在 AuthProvider 内使用');
  return ctx;
}

// 在任意子组件中使用,无需 Props
function Header() {
  const { user, logout } = useAuth();
  return (
    <header>
      <span>欢迎, {user?.name}</span>
      <button onClick={logout}>退出</button>
    </header>
  );
}

Context 的性能问题

这是 Context API 最大的坑:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,不管它们实际使用的是哪部分数据。

// ❌ 问题:每次任何一个属性变化,所有消费者都重渲染
<AppContext.Provider
  value={{ user, theme, language, notifications }}
>

// ✅ 解决方案1:拆分多个 Context(按变化频率分开)
<UserContext.Provider value={user}>
  <ThemeContext.Provider value={{ theme, setTheme }}>
    <NotificationContext.Provider value={notifications}>
      {children}
    </NotificationContext.Provider>
  </ThemeContext.Provider>
</UserContext.Provider>

// ✅ 解决方案2:用 useMemo 稳定 value
const value = useMemo(
  () => ({ user, login, logout }),
  [user, login, logout]
);
<AuthContext.Provider value={value}>

// ✅ 解决方案3:消费组件用 React.memo
const Avatar = memo(() => {
  const { user } = useAuth();
  return <img src={user?.avatar} />;
});

3. Zustand 介绍

Zustand(德语"状态")是目前最受欢迎的轻量级状态管理库之一。它的哲学是:简单、无模板代码、按需订阅

与 Redux 的对比

特性Redux ToolkitZustand
概念复杂度高(store/slice/action/selector)低(create 一个函数搞定)
样板代码多(配置、combineReducers...)极少
包大小~16kB(RTK)~1kB
TypeScript完整支持完整支持,且更简洁
DevTools官方 Redux DevToolsdevtools 中间件
最优选择大型团队、严格规范中小型项目、快速开发

安装

npm install zustand

4. Zustand 基础使用

Zustand 的核心是 create 函数——传入一个函数,返回一个自定义 Hook。状态和更新它的 actions 都定义在同一个 create 调用中。

创建 Store

import { create } from 'zustand';

// 定义 store 的类型
interface CounterStore {
  count: number;
  increment(): void;
  decrement(): void;
  reset(): void;
  incrementBy(n: number): void;
}

// create() 返回一个 Hook
const useCounterStore = create<CounterStore>()((set, get) => ({
  count: 0,

  // Actions:调用 set 更新状态
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
  reset:     () => set({ count: 0 }),

  // get() 读取当前状态(不用于 UI,用于 action 内部逻辑)
  incrementBy: (n) => set({ count: get().count + n }),
}));

在组件中使用

function Counter() {
  // 只订阅需要的部分(Selector 优化)
  const count = useCounterStore(state => state.count);
  const increment = useCounterStore(state => state.increment);
  const reset = useCounterStore(state => state.reset);

  // count 变化时此组件重渲染;increment/reset 变化时不会(函数引用稳定)
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

// 也可以在组件外部直接调用 actions(不在组件里也能用!)
useCounterStore.getState().increment();
useCounterStore.setState({ count: 100 });

完整的购物车示例

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  totalItems: number;
  totalPrice: number;
  addItem(item: Omit<CartItem, 'quantity'>): void;
  removeItem(id: string): void;
  updateQuantity(id: string, qty: number): void;
  clearCart(): void;
}

const useCartStore = create<CartStore>()((set, get) => ({
  items: [],
  get totalItems() { return get().items.reduce((s, i) => s + i.quantity, 0); },
  get totalPrice() { return get().items.reduce((s, i) => s + i.price * i.quantity, 0); },

  addItem: (item) => set(state => {
    const existing = state.items.find(i => i.id === item.id);
    if (existing) {
      return {
        items: state.items.map(i =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        )
      };
    }
    return { items: [...state.items, { ...item, quantity: 1 }] };
  }),

  removeItem: (id) => set(state => ({
    items: state.items.filter(i => i.id !== id)
  })),

  updateQuantity: (id, qty) => set(state => ({
    items: state.items.map(i => i.id === id ? { ...i, quantity: qty } : i)
  })),

  clearCart: () => set({ items: [] }),
}));

5. Zustand 进阶

Zustand 通过中间件(middleware)机制扩展功能,最常用的有 persist、devtools 和 immer。

persist 中间件 — 持久化到 localStorage

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({
      theme: 'dark',
      language: 'zh',
      fontSize: 14,
      setTheme: (theme) => set({ theme }),
      setLanguage: (lang) => set({ language: lang }),
    }),
    {
      name: 'app-settings',           // localStorage key
      storage: createJSONStorage(() => localStorage),

      // 只持久化部分字段(跳过 actions)
      partialize: (state) => ({
        theme: state.theme,
        language: state.language,
      }),
    }
  )
);

devtools 中间件 — Redux DevTools 支持

import { devtools } from 'zustand/middleware';

const useStore = create<Store>()(
  devtools(
    (set) => ({
      count: 0,
      // 第二个参数是 action 名称,在 DevTools 中显示
      increment: () => set(s => ({ count: s.count + 1 }), false, 'counter/increment'),
    }),
    { name: 'MyApp Store' }
  )
);

immer 中间件 — 可变语法写不可变更新

import { immer } from 'zustand/middleware/immer';

const useStore = create<Store>()(
  immer((set) => ({
    user: { name: 'Alice', address: { city: 'Beijing' } },
    skills: ['JS', 'TS'],

    // immer 让你用"可变"语法写,内部自动生成不可变更新
    updateCity: (city) => set(state => {
      state.user.address.city = city; // 看起来在直接修改,实际上是安全的
    }),

    addSkill: (skill) => set(state => {
      state.skills.push(skill); // 可以直接 push!
    }),
  }))
);

// 组合多个中间件
const useStore = create<Store>()(
  devtools(persist(immer((set) => ({ /* ... */ }))))
);

6. 状态设计原则

最小化状态(Minimal State)

只把真正需要的数据放进 state,其余可以从 state 派生(计算)出来的数据不要重复存储。

// ❌ 冗余状态 —— totalPrice 可以从 items 计算出来
const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0); // 重复!

// ✅ 最小化 state,派生值用 useMemo 计算
const [items, setItems] = useState([]);
const totalPrice = useMemo(
  () => items.reduce((sum, item) => sum + item.price * item.qty, 0),
  [items]
);

状态归属原则

把状态放在需要它的最低层级的组件中("状态下推")。不是所有状态都需要全局管理。

单一数据源(Single Source of Truth)

同一份数据只在一个地方维护。如果用户信息存在 Context 里,就不要同时存在 Zustand store 里——保持同步两份数据是灾难的根源。

7. 状态管理方案全面对比

根据实际场景选择合适的状态管理工具,没有"最好的",只有"最合适的"。

方案 适用场景 优点 缺点
useState 组件内部状态
表单、UI 开关
零配置,最简单 无法跨组件共享
useReducer 复杂组件内状态
多步骤表单
逻辑集中,可预测 仍是组件内状态
Context API 主题/语言/用户信息
变化不频繁的共享数据
内置,无依赖 性能问题,全量重渲染
Zustand 中小型应用全局状态
业务数据(购物车等)
极简 API,按需订阅
性能优秀
无强制规范,大团队需约定
Redux Toolkit 大型企业应用
多人协作,严格规范
生态完整,可预测
时间旅行调试
样板代码多,学习曲线陡

决策树

这个状态只有当前组件用?
  → 是:useState / useReducer

多个组件共享,但变化不频繁(主题/语言)?
  → 是:Context API

业务核心数据,频繁更新,多组件读写?
  → Zustand(推荐)或 Redux Toolkit

需要严格流程规范、时间旅行调试、大团队协作?
  → Redux Toolkit

Zustand 最佳实践总结

// ✅ 推荐的 Store 组织方式 —— 按功能拆分多个 store
// stores/useUserStore.ts
export const useUserStore = create<UserStore>()(...);

// stores/useCartStore.ts
export const useCartStore = create<CartStore>()(...);

// ✅ Selector 精确订阅,避免不必要重渲染
const username = useUserStore(state => state.user?.name);
const itemCount = useCartStore(state => state.items.length);

// ✅ Actions 定义在 store 内,而不是组件里
const addToCart = useCartStore(state => state.addItem);

// ✅ 复杂 selector 用 useShallow 批量订阅(避免每次都返回新对象)
import { useShallow } from 'zustand/react/shallow';
const { theme, language } = useSettingsStore(
  useShallow(state => ({ theme: state.theme, language: state.language }))
);

本章小结:状态管理的核心是"把对的数据放在对的地方"。从 useState 开始,Props Drilling 严重时引入 Context,业务数据复杂时用 Zustand。Zustand 的 persist 中间件轻松实现本地持久化,devtools 中间件方便调试。下一章我们进入路由管理。