跳到主要内容
预计阅读 48 分钟

状态管理 —— 从Context到Zustand

当你的应用只有三五个组件时,用 props 传数据毫不费力。但当组件树长到几十层深、十几个组件需要共享同一份用户信息或购物车数据时,你会发现自己在做一件荒唐的事——把 props 像接力棒一样层层传递,途经的中间组件根本不关心这些数据,却不得不当个”快递中转站”。这就是状态管理要解决的核心问题。本章将从 React 内置方案出发,一路走到当下最流行的 Zustand,帮你找到”刚好够用”的那把钥匙。

📋 开篇自测:你已经知道多少?

  1. 你能解释 props drilling 的含义,并说出它带来的两个以上具体问题吗?
  2. Context 配合 useReducer 已经能做状态管理了,为什么社区还是涌现了 Redux、Zustand、Jotai 这些第三方库?
  3. 如果让你为一个新项目选择状态管理方案,你的选型标准是什么?

一、为什么需要状态管理?—— props drilling 的真实痛感

1.1 一个让人抓狂的场景

假设你正在做一个电商应用,用户登录后的信息(用户名、头像、会员等级)需要在以下地方使用:顶部导航栏、侧边栏、商品详情页的”专属价格”、购物车页面、个人中心。这些组件散落在组件树的各个角落。

如果只用 props 传递,组件树大致像这样:

function App() {
  const [user, setUser] = useState({ name: '小明', level: 'vip' });

  return (
    <Layout user={user}>           {/* Layout 自己不用 user */}
      <Header user={user}>         {/* Header 只用了 user.name */}
        <NavBar user={user}>       {/* NavBar 不用 user,但要往下传 */}
          <UserAvatar user={user} /> {/* 终于用到了 */}
        </NavBar>
      </Header>
      <Content user={user}>        {/* Content 不用 user */}
        <ProductPage user={user}>   {/* ProductPage 不用 user */}
          <PriceTag user={user} />  {/* 终于用到了 user.level */}
        </ProductPage>
      </Content>
    </Layout>
  );
}

这就是 props drilling(属性穿透)——数据像打井一样,必须穿过一层又一层不相干的岩层才能到达目标。

1.2 Props drilling 的三大痛点

痛点一:中间组件被”污染”。 Layout、Content、NavBar 这些组件本来和 user 毫无关系,却被迫接受并传递一个自己用不上的 prop。一旦 user 的结构发生变化(比如新增一个 avatar 字段),所有中间组件的类型定义都要跟着改。

痛点二:重构成本高。 如果你想把 UserAvatar 从 NavBar 移到 Sidebar 下面,整条 props 传递链都得重新布线。

痛点三:不必要的重渲染。user 变化时,整条传递链上的所有组件都会重新渲染——即使它们只是”路过”而已。

打一个比方:props drilling 就像快递必须从北京总仓发到省仓、市仓、区仓、站点,最后才到你手里。如果每个包裹都要走这条链路,效率极低。而状态管理就像京东的前置仓——货直接放在离你最近的地方,需要的人直接去取。

🤔 想一想 如果一个应用只有 2-3 层组件嵌套,你还会选择引入状态管理库吗?过度设计和设计不足,你更害怕哪个?


二、Context + useReducer —— React 内置的”够用”方案

在引入第三方库之前,先看看 React 自己能做到什么程度。

2.1 Context:跨层传递数据

createContext + useContext 可以让数据跳过中间组件直达目标:

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

// 1. 创建 Context
const UserContext = createContext(null);

// 2. 在顶层提供数据
function App() {
  const [user, setUser] = useState({ name: '小明', level: 'vip' });

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Layout>
        <Header>
          <NavBar>
            <UserAvatar />   {/* 不需要 props 了 */}
          </NavBar>
        </Header>
      </Layout>
    </UserContext.Provider>
  );
}

// 3. 在任意深度消费数据
function UserAvatar() {
  const { user } = useContext(UserContext);
  return <span>你好,{user.name}</span>;
}

中间的 Layout、Header、NavBar 完全不用感知 user 的存在。这就是 Context 的核心价值——穿透组件层级,直达消费者

2.2 配合 useReducer 管理复杂状态

当状态逻辑变复杂(多个 action、状态之间有依赖),useState 就捉襟见肘了。useReducer 提供了类似 Redux 的 dispatch/action 模式:

import { createContext, useContext, useReducer } from 'react';

// 定义 reducer
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      throw new Error(`未知 action: ${action.type}`);
  }
}

// 创建 Context
const CounterContext = createContext(null);

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

// 消费者组件
function Counter() {
  const { state, dispatch } = useContext(CounterContext);

  return (
    <div>
      <h2>计数:{state.count}</h2>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

2.3 Context 的性能痛点

Context 方案看起来不错,但有一个关键缺陷:Provider 的 value 变化时,所有消费者都会重新渲染,无论它们是否用到了变化的那部分数据。

const AppContext = createContext(null);

function AppProvider({ children }) {
  const [user, setUser] = useState({ name: '小明' });
  const [theme, setTheme] = useState('light');

  // user 和 theme 放在同一个 Context 里
  return (
    <AppContext.Provider value={{ user, setUser, theme, setTheme }}>
      {children}
    </AppContext.Provider>
  );
}

// 这个组件只用了 theme
function ThemeToggle() {
  const { theme, setTheme } = useContext(AppContext);
  console.log('ThemeToggle 渲染了'); // user 变化时也会打印!

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      当前主题:{theme}
    </button>
  );
}

user 更新时,ThemeToggle 明明只用了 theme,却也被迫重新渲染。这在小项目中无所谓,但当消费者组件变多、渲染开销变大时,就会成为性能瓶颈。

常见的缓解方式: 把不同领域的状态拆到不同的 Context 里(一个 UserContext、一个 ThemeContext)。但如果状态之间有交叉依赖,拆分就变得不现实了。

小结: Context + useReducer 适合中小型应用或组件库内部的状态共享。当你发现自己在拆分 Context、手动做 memo 优化、状态之间耦合严重时,就是时候考虑第三方方案了。


三、Redux Toolkit —— 经典方案的现代写法

Redux 是 React 生态中最早也最知名的状态管理库。早期的 Redux 以”写法繁琐”闻名——定义一个功能需要分别写 actionType、actionCreator、reducer 三个文件。Redux Toolkit(RTK)是 Redux 官方推出的”最佳实践”封装,大幅减少了样板代码。

3.1 用 Redux Toolkit 实现计数器

先安装依赖:

npm install @reduxjs/toolkit react-redux

createSlice 同时定义 state、reducers 和 actions:

// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment(state) {
      state.count += 1;  // RTK 内部用 Immer,可以直接"修改"状态(注意:只有在 RTK 的 createSlice 中才能这样写,普通 Redux reducer 不行)
    },
    decrement(state) {
      state.count -= 1;
    },
    reset(state) {
      state.count = 0;
    },
  },
});

export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;

configureStore 创建 store:

// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

在组件中使用:

// App.jsx
import { Provider, useSelector, useDispatch } from 'react-redux';
import { store } from './store';
import { increment, decrement, reset } from './store/counterSlice';

function Counter() {
  const count = useSelector((state) => state.counter.count);
  const dispatch = useDispatch();

  return (
    <div>
      <h2>计数:{count}</h2>
      <button onClick={() => dispatch(increment())}>+1</button>
      <button onClick={() => dispatch(decrement())}>-1</button>
      <button onClick={() => dispatch(reset())}>重置</button>
    </div>
  );
}

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

3.2 RTK 的优势与代价

优势:

  • 内置 Immer,可以用”看起来可变”的语法写不可变更新
  • 内置 DevTools 支持,时间旅行调试
  • 拥有庞大的生态系统(RTK Query 处理异步请求、中间件系统)
  • 适合大团队——严格的单向数据流,约束明确

代价:

  • 概念较多:store、slice、action、dispatch、selector
  • 即使有了 RTK,仍然需要 Provider 包裹
  • 对于简单场景来说仍然”太重”——一个计数器需要 3 个文件

🤔 想一想 Redux 的核心设计哲学是”可预测的状态变更”——所有状态修改必须通过 dispatch action 触发,这像不像银行转账必须走柜台流程?这种约束在什么规模的项目中才真正有价值?


四、Zustand —— 极简状态管理

Zustand(德语”状态”的意思)是近几年增长最快的 React 状态管理库。它的设计哲学可以用一句话概括:能少写一行代码就少写一行。

4.1 用 Zustand 实现同样的计数器

安装:

npm install zustand

定义 store:

// store/useCounterStore.js
import { create } from 'zustand';

const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

export default useCounterStore;

在组件中使用:

// App.jsx
import useCounterStore from './store/useCounterStore';

function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);
  const reset = useCounterStore((state) => state.reset);

  return (
    <div>
      <h2>计数:{count}</h2>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

function App() {
  return <Counter />;  // 不需要 Provider!
}

对比一下代码量:Context 方案大约 40 行,Redux Toolkit 大约 35 行(分散在 3 个文件),Zustand 大约 15 行(1 个文件)

4.2 Zustand 为什么增长这么快?

第一,不需要 Provider。 Zustand 的 store 是一个独立于 React 组件树的外部存储。这意味着你可以在任何地方(包括 React 组件外部的工具函数里)读写状态,不受组件层级限制。

// 甚至可以在组件外部读写状态
const currentCount = useCounterStore.getState().count;
useCounterStore.getState().increment();

第二,精准的重渲染控制。 使用选择器(selector)只订阅需要的部分状态,其他状态变化不会触发组件重渲染:

// 只订阅 count,increment 变化不会导致重渲染
const count = useCounterStore((state) => state.count);

第三,几乎零概念。 没有 action type、没有 dispatch、没有 reducer,store 就是一个包含状态和方法的普通对象。

第四,体积极小。 Zustand gzip 后仅约 1.2KB,而 Redux Toolkit + react-redux 加在一起 tree-shaken 后约 11-14KB,体积差距显著。

4.3 Zustand 的中间件

Zustand 虽然简单,但并不简陋。它提供了强大的中间件系统:

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

const useCounterStore = create(
  devtools(
    persist(
      (set) => ({
        count: 0,
        increment: () => set(
          (state) => ({ count: state.count + 1 }),
          false,
          'increment'  // devtools 中显示的 action 名称
        ),
      }),
      { name: 'counter-storage' }  // localStorage 的 key
    )
  )
);
  • devtools:接入 Redux DevTools 浏览器扩展,支持时间旅行调试
  • persist:自动将状态持久化到 localStorage,页面刷新后状态不丢失
  • immer:允许用可变语法写不可变更新(和 RTK 类似)

五、Jotai —— 原子化状态管理

如果说 Zustand 是”把整个仓库打包成一个 store”,那么 Jotai 的思路完全相反——把状态拆成一个个最小的原子(atom),按需组合

5.1 用 Jotai 实现计数器

安装:

npm install jotai
import { atom, useAtom } from 'jotai';

// 定义原子状态
const countAtom = atom(0);

function Counter() {
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      <h2>计数:{count}</h2>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <button onClick={() => setCount((c) => c - 1)}>-1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

5.2 原子化的威力:派生状态

Jotai 真正的亮点在于原子的组合——你可以从已有的原子派生出新的原子:

const countAtom = atom(0);

// 派生只读原子:自动跟踪 countAtom 的变化
const doubledAtom = atom((get) => get(countAtom) * 2);

// 派生读写原子
const countWithLimitAtom = atom(
  (get) => get(countAtom),
  (get, set, newValue) => {
    // 限制 count 不超过 100
    set(countAtom, Math.min(newValue, 100));
  }
);

function Display() {
  const [doubled] = useAtom(doubledAtom);
  return <p>双倍值:{doubled}</p>;  // 自动响应 countAtom 的变化
}

原子化的优势: 每个组件只订阅自己真正依赖的原子,状态变化时只有相关组件重渲染,天然避免了 Context 的”全量通知”问题。

适合场景: 状态之间有复杂的派生关系,或者你希望状态的定义和组件解耦、可以在任意地方自由组合。


六、方案选型指南 —— 什么项目用什么方案

说了这么多方案,到底该怎么选?先看一张对比表:

维度Context + useReducerRedux ToolkitZustandJotai
额外依赖无(React 内置)@reduxjs/toolkit + react-reduxzustandjotai
gzip 体积0 KB~11-14 KB~1 KB~4 KB
学习成本中高极低
需要 Provider可选
DevTools内置中间件支持官方扩展
异步处理需要手动处理RTK Query直接在 action 中写支持异步 atom
精准重渲染差(整个 Context 消费者都更新)好(useSelector)好(selector)好(原子级别)
TypeScript 支持需要手动定义类型自动推断自动推断自动推断
组件外访问不支持store.getState()store.getState()需要自建 store

6.1 选型决策树

按照以下思路选择,通常不会出错:

小型项目(个人博客、工具页、5 个以下共享状态): Context + useReducer 足够了,无需引入额外依赖。

中型项目(后台管理系统、电商前端、团队 3-5 人): Zustand 是当下的最优选。API 简单、体积小、性能好,上手几乎零成本。

大型项目(企业级复杂应用、团队 10+人、需要严格的代码规范): Redux Toolkit 仍然是稳妥选择。它的约束性(必须通过 action 修改状态)在大团队中反而是优势——代码可追溯、可审计。

状态关系复杂(大量派生状态、类似电子表格的联动计算): Jotai 的原子化模型天然适合这种场景。

🤔 想一想 你目前正在做或将要做的项目,大致属于上面哪个规模?如果让你现在选一个方案,你会选哪个?试着说出你的理由。


七、实战:用 Zustand 构建购物车状态管理

纸上得来终觉浅。下面我们用 Zustand 实现一个功能完整的购物车——包含添加商品、修改数量、删除商品、计算总价。

7.1 定义购物车 Store

// store/useCartStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useCartStore = create(
  devtools(
    persist(
      (set, get) => ({
        // 状态
        items: [],
        // items 的结构:[{ id, name, price, quantity }]

        // 添加商品
        addItem: (product) => set((state) => {
          const existing = state.items.find((item) => item.id === product.id);
          if (existing) {
            // 已存在,数量 +1
            return {
              items: state.items.map((item) =>
                item.id === product.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              ),
            };
          }
          // 不存在,新增
          return { items: [...state.items, { ...product, quantity: 1 }] };
        }),

        // 修改数量
        updateQuantity: (id, quantity) => set((state) => {
          if (quantity <= 0) {
            // 数量为 0 则移除
            return { items: state.items.filter((item) => item.id !== id) };
          }
          return {
            items: state.items.map((item) =>
              item.id === id ? { ...item, quantity } : item
            ),
          };
        }),

        // 删除商品
        removeItem: (id) => set((state) => ({
          items: state.items.filter((item) => item.id !== id),
        })),

        // 清空购物车
        clearCart: () => set({ items: [] }),

        // 计算总价(使用 get 读取当前状态)
        getTotalPrice: () => {
          const { items } = get();
          return items.reduce(
            (total, item) => total + item.price * item.quantity,
            0
          );
        },

        // 计算总数量
        getTotalCount: () => {
          const { items } = get();
          return items.reduce((total, item) => total + item.quantity, 0);
        },
      }),
      { name: 'shopping-cart' }  // 持久化到 localStorage
    ),
    { name: 'CartStore' }  // DevTools 中显示的名称
  )
);

export default useCartStore;

7.2 商品列表组件

// components/ProductList.jsx
import useCartStore from '../store/useCartStore';

const PRODUCTS = [
  { id: 1, name: 'React 实战教程', price: 99 },
  { id: 2, name: 'TypeScript 入门', price: 79 },
  { id: 3, name: 'Node.js 进阶', price: 89 },
];

function ProductList() {
  const addItem = useCartStore((state) => state.addItem);

  return (
    <div>
      <h2>商品列表</h2>
      {PRODUCTS.map((product) => (
        <div key={product.id} style={{ display: 'flex', gap: 12, marginBottom: 8 }}>
          <span>{product.name}</span>
          <span>¥{product.price}</span>
          <button onClick={() => addItem(product)}>加入购物车</button>
        </div>
      ))}
    </div>
  );
}

export default ProductList;

7.3 购物车组件

// components/Cart.jsx
import useCartStore from '../store/useCartStore';

function Cart() {
  const items = useCartStore((state) => state.items);
  const updateQuantity = useCartStore((state) => state.updateQuantity);
  const removeItem = useCartStore((state) => state.removeItem);
  const clearCart = useCartStore((state) => state.clearCart);
  const getTotalPrice = useCartStore((state) => state.getTotalPrice);
  const getTotalCount = useCartStore((state) => state.getTotalCount);

  if (items.length === 0) {
    return <p>购物车是空的,去选购一些课程吧!</p>;
  }

  return (
    <div>
      <h2>购物车({getTotalCount()} 件)</h2>
      {items.map((item) => (
        <div key={item.id} style={{ display: 'flex', gap: 12, marginBottom: 8 }}>
          <span>{item.name}</span>
          <span>¥{item.price}</span>
          <div>
            <button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
            <span style={{ margin: '0 8px' }}>{item.quantity}</span>
            <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
          </div>
          <button onClick={() => removeItem(item.id)}>删除</button>
        </div>
      ))}
      <hr />
      <p><strong>总计:¥{getTotalPrice()}</strong></p>
      <button onClick={clearCart}>清空购物车</button>
    </div>
  );
}

export default Cart;

7.4 导航栏中的购物车角标

Zustand 的精准订阅在这里体现得淋漓尽致——导航栏只关心商品总数,不关心具体商品信息。当用户修改某个商品的名称或价格时,导航栏不会重渲染。

// components/CartBadge.jsx
import useCartStore from '../store/useCartStore';

function CartBadge() {
  // 直接在 selector 中计算派生值,Zustand 会在值变化时触发重渲染
  const totalCount = useCartStore((state) =>
    state.items.reduce((total, item) => total + item.quantity, 0)
  );

  return (
    <span style={{
      background: totalCount > 0 ? '#ff4d4f' : '#ccc',
      color: '#fff',
      borderRadius: '50%',
      padding: '2px 8px',
      fontSize: 12,
    }}>
      {totalCount}
    </span>
  );
}

export default CartBadge;

7.5 组装完整应用

// App.jsx
import ProductList from './components/ProductList';
import Cart from './components/Cart';
import CartBadge from './components/CartBadge';

function App() {
  return (
    <div style={{ padding: 24 }}>
      <nav style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
        <h1>课程商城</h1>
        <div>购物车 <CartBadge /></div>
      </nav>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
        <ProductList />
        <Cart />
      </div>
    </div>
  );
}

export default App;

注意整个应用中没有任何 Provider 包裹。ProductList 和 Cart 完全解耦——它们各自通过 useCartStore 读写同一份状态,彼此不需要知道对方的存在。这就是 Zustand “去中心化”的魅力。

7.6 在组件外部访问状态

购物车还有一个常见需求:在发起支付请求时(通常在一个工具函数里),需要读取购物车数据。Zustand 可以在组件外直接访问:

// utils/checkout.js
import useCartStore from '../store/useCartStore';

export async function checkout() {
  const { items, getTotalPrice, clearCart } = useCartStore.getState();

  const response = await fetch('/api/orders', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      items,
      totalPrice: getTotalPrice(),
    }),
  });

  if (response.ok) {
    clearCart();  // 支付成功,清空购物车
    return true;
  }
  return false;
}

如果你用的是 Context 方案,这段代码根本无法实现——因为 Context 只能在 React 组件内部通过 useContext 访问。这也是 Zustand 在工程实践中的一大优势。


📝 掌握度自测

  1. 以下哪个不是 props drilling 带来的问题?

    • A) 中间组件被迫接受不相关的 props
    • B) 重构组件位置时需要重新布线 props 传递链
    • C) 组件的 state 被全局共享导致安全问题
    • D) 不必要的中间组件重渲染
  2. React Context 方案的主要性能缺陷是什么?

    • A) Context 的 value 无法传递函数
    • B) Provider 的 value 变化时,所有消费者组件都会重渲染
    • C) Context 不支持 TypeScript
    • D) Context 不能嵌套使用
  3. 以下关于 Zustand 的描述,哪个是错误的?

    • A) 不需要 Provider 包裹组件树
    • B) 可以在 React 组件外部读写状态
    • C) 必须配合 Redux DevTools 才能使用
    • D) gzip 后体积约 1KB
  4. Jotai 的”原子化”状态管理,其核心优势是什么?

    • A) 比 Zustand 体积更小
    • B) 状态按原子粒度订阅,天然避免不必要的重渲染
    • C) 不需要安装任何依赖
    • D) 只支持同步状态
  5. 在一个 10 人团队开发的企业级复杂后台系统中,以下哪个选择最合理?

    • A) 只用 useState,坚决不引入状态管理
    • B) 用 Context + useReducer,保持零依赖
    • C) 用 Redux Toolkit,利用其约束性保证代码规范
    • D) 用 Jotai,因为它最新最潮

💡 自我评估

  • 答对 5 题:你已经对 React 状态管理的全局图景有了清晰的认识,可以根据项目需求做出合理的技术选型。
  • 答对 3-4 题:基础不错,建议回顾 Context 的性能缺陷和各方案的适用场景,这是面试高频考点。
  • 答对 0-2 题:建议把文中的计数器示例分别用 Context、Redux Toolkit、Zustand 实现一遍,动手写过才能真正理解差异。

参考答案: 1-C, 2-B, 3-C, 4-B, 5-C

购买课程解锁全部内容

从组件到架构:12 章系统掌握现代 React

¥29.90