React Context API 完全指南:跨组件状态共享

小飞兽 React 16 次阅读 2026-05-16

React Context API 完全指南:跨组件状态共享

一、Introduction

在 React 应用中,数据通常沿着组件树从上往下传递(props drilling):顶层组件持有状态,通过 props 一层层往下传,即使中间某些组件根本不需要这个数据,也不得不当"传话筒"。Context API 解决这个问题:它允许你在组件树中创建"全局数据通道",让任何层级的组件直接访问和修改共享状态,而不需要通过中间组件传递。

Context 不是万能的——它会使得组件复用性降低,所以应该用在真正全局需要的数据上,比如:当前登录用户、主题、语言设置、全局通知等。本文详解 Context 的完整用法、创建模式、以及常见陷阱。

---

二、Context 的基本用法

#### 2.1 创建与使用 Context

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

// 第一步:创建一个 Context 对象
// 可以传入默认值(不提供时使用)
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
});

const UserContext = createContext(null);

// 第二步:创建 Provider 组件,包裹需要共享数据的组件树
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: '游客', level: 0 });

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    // 使用 Provider 的 value 属性传递要共享的数据
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <UserContext.Provider value={user}>
        <div
          style={{
            background: theme === 'light' ? '#fff' : '#222',
            color: theme === 'light' ? '#333' : '#eee',
            minHeight: '100vh',
            padding: 20
          }}
        >
          <h1>React Context 演示</h1>
          <NavBar />
          <ThemeToggle />
          <UserInfo />
        </div>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 第三步:在需要的组件中用 useContext 获取数据
function NavBar() {
  const { theme } = useContext(ThemeContext);
  return (
    <nav style={{ borderBottom: `1px solid ${theme === 'light' ? '#ddd' : '#555'}`, paddingBottom: 12, marginBottom: 16 }}>
      导航栏 | 当前主题:{theme}
    </nav>
  );
}

function ThemeToggle() {
  // 从 Context 获取数据和操作方法
  const { theme, toggleTheme } = useContext(ThemeContext);
  return (
    <button
      onClick={toggleTheme}
      style={{
        padding: '8px 16px',
        background: theme === 'light' ? '#333' : '#f0f0f0',
        color: theme === 'light' ? '#fff' : '#333',
        border: 'none',
        borderRadius: 4,
        cursor: 'pointer'
      }}
    >
      切换为{theme === 'light' ? '深色' : '浅色'}主题
    </button>
  );
}

function UserInfo() {
  // 独立的 User Context,不受 Theme Context 影响
  const user = useContext(UserContext);
  return (
    <div style={{ marginTop: 16 }}>
      <p>当前用户:{user.name}(等级 {user.level})</p>
    </div>
  );
}

export default App;

---

三、Reducer + Context:重构复杂状态

#### 3.1 用 useReducer 管理复杂状态逻辑

当 Context 中的状态逻辑变得复杂时,应该把状态管理逻辑从组件中抽离出来,用 useReducer 配合 Context:

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

// ==================== 购物车状态管理 ====================

// 购物车状态初始值
const initialCartState = {
  items: [],       // [{ id, name, price, quantity }]
  totalPrice: 0,
  totalItems: 0
};

// reducer:接收当前状态和 action,返回新状态
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingIndex = state.items.findIndex(i => i.id === action.payload.id);
      let newItems;

      if (existingIndex >= 0) {
        // 商品已存在,数量+1
        newItems = state.items.map((item, idx) =>
          idx === existingIndex
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        // 新商品
        newItems = [...state.items, { ...action.payload, quantity: 1 }];
      }

      return {
        items: newItems,
        totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
        totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0)
      };
    }

    case 'REMOVE_ITEM': {
      const newItems = state.items.filter(i => i.id !== action.payload.id);
      return {
        items: newItems,
        totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
        totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0)
      };
    }

    case 'UPDATE_QUANTITY': {
      const newItems = state.items.map(item =>
        item.id === action.payload.id
          ? { ...item, quantity: Math.max(1, action.payload.quantity) }
          : item
      );
      return {
        items: newItems,
        totalPrice: newItems.reduce((sum, i) => sum + i.price * i.quantity, 0),
        totalItems: newItems.reduce((sum, i) => sum + i.quantity, 0)
      };
    }

    case 'CLEAR_CART':
      return initialCartState;

    default:
      return state;
  }
}

// ==================== Context 定义 ====================

const CartContext = createContext(null);

// ==================== Provider 组件 ====================

const PRODUCTS = [
  { id: 1, name: 'React 进阶', price: 99 },
  { id: 2, name: 'TypeScript 实战', price: 79 },
  { id: 3, name: 'Node.js 深入', price: 129 },
  { id: 4, name: 'GraphQL 指南', price: 59 },
];

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialCartState);

  // 用 useCallback 包裹,确保 dispatch 引用稳定
  const addItem = useCallback((product) => {
    dispatch({ type: 'ADD_ITEM', payload: product });
  }, []);

  const removeItem = useCallback((id) => {
    dispatch({ type: 'REMOVE_ITEM', payload: { id } });
  }, []);

  const updateQuantity = useCallback((id, quantity) => {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
  }, []);

  const clearCart = useCallback(() => {
    dispatch({ type: 'CLEAR_CART' });
  }, []);

  const value = {
    cart: state,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    products: PRODUCTS
  };

  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

// ==================== 消费组件 ====================

function ProductList() {
  const { products, addItem } = useContext(CartContext);

  return (
    <div>
      <h3>商品列表</h3>
      <ul>
        {products.map(product => (
          <li key={product.id} style={{ marginBottom: 8 }}>
            {product.name} - ¥{product.price}
            <button
              onClick={() => addItem(product)}
              style={{ marginLeft: 12 }}
            >
              加入购物车
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

function CartSummary() {
  const { cart, removeItem, updateQuantity, clearCart } = useContext(CartContext);

  if (cart.items.length === 0) {
    return <p>购物车为空</p>;
  }

  return (
    <div>
      <h3>购物车({cart.totalItems} 件商品)</h3>
      <ul>
        {cart.items.map(item => (
          <li key={item.id} style={{ marginBottom: 8 }}>
            {item.name} - ¥{item.price} × {item.quantity}
            <button onClick={() => updateQuantity(item.id, item.quantity - 1)} style={{ marginLeft: 8 }}>−</button>
            <button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
            <button onClick={() => removeItem(item.id)} style={{ marginLeft: 8, color: 'red' }}>删除</button>
          </li>
        ))}
      </ul>
      <p><strong>总价:¥{cart.totalPrice}</strong></p>
      <button onClick={clearCart}>清空购物车</button>
    </div>
  );
}

function ShoppingCartApp() {
  return (
    <CartProvider>
      <div style={{ display: 'flex', gap: 40 }}>
        <ProductList />
        <CartSummary />
      </div>
    </CartProvider>
  );
}

export default ShoppingCartApp;

---

四、运行效果说明

ThemeContext 示例:点击"切换为深色主题"按钮,NavBar、ThemeToggle、UserInfo 的背景色和文字颜色全部改变,因为它们都从 ThemeContext 读取主题数据。UserInfo 组件同时也从 UserContext 读取用户数据,两者独立不干扰。

ShoppingCartApp 示例:点击商品旁的"加入购物车",购物车实时更新商品数量和总价;点击 +/- 按钮调整数量;点击"删除"移除商品;点击"清空购物车"重置整个购物车状态。所有状态由 useReducer 统一管理,逻辑清晰可预测。

---

五、常见问题(FAQ)

#### Q1:Context 的 value 变化时,所有消费组件都会重新渲染吗?

是的。 这是 Context 最大的性能问题——Provider 下的所有消费组件,只要引用了那个 Context,不管用的是不是变化的部分,都会在 Provider 的 value 变化时重新渲染。解决方案有三种:

1. 拆分 Context:把经常变化的状态和很少变化的状态拆分到不同的 Context 中
2. 用 memo 包装消费组件:减少不必要的渲染
3. 使用第三方状态管理库(Zustand、Jotai)代替 Context 处理频繁变化的状态

// 不好:一个 Context 包含所有数据,任何变化都导致全部重新渲染
const AppContext = createContext({ theme, user, notifications, cart });

// 好:拆分为多个 Context,变化隔离
const ThemeContext = createContext({ theme });
const UserContext = createContext({ user });
const NotificationContext = createContext({ notifications });
const CartContext = createContext({ cart });

#### Q2:可以在 Context Provider 外部使用 useContext 吗?

可以,但会使用默认值。 createContext() 的参数是默认值,仅在 Provider 树外部消费 Context 时使用(通常用于独立测试组件)。实际应用中,在 Provider 树内使用时默认值被忽略。

const ThemeContext = createContext('light'); // 默认值

// 在 Provider 外使用时,返回默认值 'light'
function StandaloneButton() {
  const theme = useContext(ThemeContext); // theme === 'light'
  return <button style={{ background: theme === 'light' ? '#fff' : '#333' }}>按钮</button>;
}

#### Q3:Context 的 Provider 可以嵌套使用吗?

可以。 同一个 Context 类型可以多次 Provider,后续 Provider 会覆盖前面的值:

<ThemeContext.Provider value={{ theme: 'light' }}>
  <div>这里用浅色主题</div>
  <ThemeContext.Provider value={{ theme: 'dark' }}>
    <div>这里用深色主题,覆盖了外层</div>
  </ThemeContext.Provider>
</ThemeContext.Provider>

---

六、延伸阅读

  • [React Hooks 完全指南:useState 与 useEffect 核心用法]()
  • [React 自定义 Hooks 实战:逻辑复用与模块化]()
  • [React 状态管理演进:从 useState 到 Redux]()
  • [React useCallback 与 useMemo 性能优化完全指南]()

---