react

Context 与全局状态

By AI-Writer 9 min read

前言

Context 是 React 提供的跨组件数据传递机制,允许你在组件树中传递数据,而无需在每一层手动传递 props。当数据需要在多个层级的组件间共享时,Context 是比”层层 prop drilling”更优雅的解决方案。

Context 基础

创建 Context

jsx
import { createContext } from 'react';

// 创建 Theme Context,默认值可自定义
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
});

export default ThemeContext;

Provider 提供者

jsx
import ThemeContext from './ThemeContext';

function App() {
  const [theme, setTheme] = useState('light');

  const value = {
    theme,
    toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
  };

  return (
    // 使用 Provider 包裹组件树
    <ThemeContext.Provider value={value}>
      <MainPage />
    </ThemeContext.Provider>
  );
}

消费 Context

jsx
import { useContext } from 'react';
import ThemeContext from './ThemeContext';

function Header() {
  // 使用 useContext 获取 Context 值
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <header className={`theme-${theme}`}>
      <h1>我的应用</h1>
      <button onClick={toggleTheme}>
        当前主题:{theme}
      </button>
    </header>
  );
}

Context 完整示例

主题切换系统

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

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

// 2. Provider 组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

  const value = { theme, toggleTheme };

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

// 3. 自定义 Hook(简化消费)
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用');
  }
  return context;
}

// 4. 消费组件
function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === 'dark' ? '#333' : '#fff',
        color: theme === 'dark' ? '#fff' : '#333'
      }}
    >
      切换为 {theme === 'light' ? '深色' : '浅色'} 主题
    </button>
  );
}

function ThemedBox() {
  const { theme } = useTheme();

  return (
    <div
      style={{
        background: theme === 'dark' ? '#222' : '#f5f5f5',
        color: theme === 'dark' ? '#fff' : '#333',
        padding: '2rem',
        marginTop: '1rem'
      }}
    >
      这是 {theme === 'dark' ? '深色' : '浅色'} 主题的盒子
    </div>
  );
}

// 5. 使用
function App() {
  return (
    <ThemeProvider>
      <ThemeToggle />
      <ThemedBox />
    </ThemeProvider>
  );
}

Context 性能优化

Context 的一个常见问题是:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染。

问题:每次 value 变化都重新渲染

jsx
function ProblematicProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  // ❌ 问题:每次 theme 变化,UserProfile 也会重新渲染
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        {children}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

方案 1:分离 Context

jsx
// 分别创建两个 Context
const UserContext = createContext();
const ThemeContext = createContext();

// 分离的 Provider
<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    {children}
  </ThemeContext.Provider>
</UserContext.Provider>

方案 2:使用 useMemo 稳定 value

jsx
function OptimizedProvider({ children }) {
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState([]);

  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const notificationValue = useMemo(
    () => ({ notifications, addNotification: (msg) => setNotifications(prev => [...prev, msg]) }),
    [notifications]
  );

  return (
    <UserContext.Provider value={userValue}>
      <NotificationContext.Provider value={notificationValue}>
        {children}
      </NotificationContext.Provider>
    </UserContext.Provider>
  );
}

方案 3:分离 context 为多个文件

jsx
// contexts/ThemeContext.tsx
const ThemeContext = createContext();
export const useTheme = () => useContext(ThemeContext);
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// contexts/UserContext.tsx
const UserContext = createContext();
export const useUser = () => useContext(UserContext);
export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

// App.tsx
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <Router />
      </UserProvider>
    </ThemeProvider>
  );
}

useContext 使用技巧

获取部分值

jsx
function NotificationBadge() {
  // ❌ 获取整个 context(可能触发不必要渲染)
  const { user, theme, notifications } = useAppContext();

  // ✓ 只获取需要的内容
  const { unreadCount } = useAppContext();
}

组合多个 Context

jsx
function UserProfile() {
  // 分别消费不同的 Context
  const { user } = useUser();
  const { theme } = useTheme();

  if (!user) return null;

  return (
    <div className={`profile-${theme}`}>
      <Avatar src={user.avatar} />
      <span>{user.name}</span>
    </div>
  );
}

使用 selector 模式

jsx
// 自定义 Hook:只获取部分 context 值
function useContextSelector(context, selector) {
  const ctx = useContext(context);
  const selected = selector(ctx);

  const [, forceUpdate] = useState({});
  const prevSelected = useRef(selected);

  if (selected !== prevSelected.current) {
    prevSelected.current = selected;
    forceUpdate({});
  }

  return selected;
}

// 使用
function NotificationBadge() {
  const unreadCount = useContextSelector(
    NotificationContext,
    ctx => ctx.notifications.filter(n => !n.read).length
  );

  return unreadCount > 0 ? <span>{unreadCount}</span> : null;
}

Context 与状态提升的选择

用 Context 的场景

plaintext
✅ 全局状态:用户登录信息、主题、语言设置
✅ 跨层级传递:爷组件 → 孙组件,不需要中间组件传递
✅ 多人协作:不同团队维护不同组件,共享数据需要明确的 API

用状态提升的场景

plaintext
✅ 父子通信:父组件持有状态,子组件通过 props 接收
✅ 兄弟通信:通过父组件协调
✅ 局部状态:只在少数组件中使用
✅ 简单场景:不需要 Context 的额外复杂度

示例对比

jsx
// 状态提升(适合简单场景)
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <Display count={count} />
      <Increment onIncrement={() => setCount(c => c + 1)} />
    </>
  );
}

// Context(适合跨组件共享)
function App() {
  return (
    <AuthProvider>       {/* 全应用的用户登录状态 */}
      <ThemeProvider>    {/* 全应用的主题设置 */}
        <Router />       {/* 路由 */}
      </ThemeProvider>
    </AuthProvider>
  );
}

完整示例:购物车状态

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

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

// 2. Reducer 定义
const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      const existingIndex = state.items.findIndex(
        item => item.id === action.payload.id
      );
      if (existingIndex >= 0) {
        const newItems = [...state.items];
        newItems[existingIndex].quantity += 1;
        return { ...state, items: newItems };
      }
      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }]
      };

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        )
      };

    case 'CLEAR_CART':
      return { ...state, items: [] };

    default:
      return state;
  }
};

// 3. Provider 组件
export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  const addItem = (product) => dispatch({ type: 'ADD_ITEM', payload: product });
  const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id });
  const updateQuantity = (id, quantity) =>
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
  const clearCart = () => dispatch({ type: 'CLEAR_CART' });

  const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = state.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  const value = {
    items: state.items,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    totalItems,
    totalPrice
  };

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

// 4. 自定义 Hook
export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart 必须在 CartProvider 内使用');
  }
  return context;
}

// 5. 消费组件
function CartIcon() {
  const { totalItems } = useCart();
  return totalItems > 0 ? <span className="badge">{totalItems}</span> : null;
}

function CartPage() {
  const { items, removeItem, updateQuantity, clearCart, totalPrice } = useCart();

  return (
    <div>
      <h1>购物车</h1>
      {items.length === 0 ? (
        <p>购物车是空的</p>
      ) : (
        <>
          <ul>
            {items.map(item => (
              <li key={item.id}>
                <span>{item.name}</span>
                <span>¥{item.price}</span>
                <input
                  type="number"
                  value={item.quantity}
                  onChange={e => updateQuantity(item.id, parseInt(e.target.value))}
                />
                <button onClick={() => removeItem(item.id)}>删除</button>
              </li>
            ))}
          </ul>
          <p>总价:¥{totalPrice.toFixed(2)}</p>
          <button onClick={clearCart}>清空购物车</button>
        </>
      )}
    </div>
  );
}

Context 的注意事项

Context 替换 Prop Drilling

jsx
// ❌ Prop Drilling:层层传递
function App() {
  const [user] = useState({ name: 'Alice', role: 'admin' });
  return <Page user={user} />;
}

function Page({ user }) {
  return <Dashboard user={user} />;
}

function Dashboard({ user }) {
  return <Profile user={user} />;
}

function Profile({ user }) {
  return <h1>{user.name}</h1>;
}

// ✓ 使用 Context
const UserContext = createContext();

function App() {
  const [user] = useState({ name: 'Alice', role: 'admin' });
  return (
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  );
}

function Profile() {
  const user = useContext(UserContext);
  return <h1>{user.name}</h1>;
}

Context 不是 Redux

plaintext
Context 适用场景:
  ✅ 主题、语言、认证状态
  ✅ 低频更新的全局配置
  ✅ 简单的状态共享

Context 不适用场景:
  ❌ 复杂的状态逻辑(考虑用 useReducer + Context)
  ❌ 高频更新的数据(考虑状态管理库)
  ❌ 需要中间件、devtools(考虑 Redux/Zustand)

小结

  • Context:React 提供的跨组件数据传递机制,通过 Provider 和 useContext 配合使用
  • Provider:在组件树中提供数据,使用 value prop 传递值
  • useContext:在消费组件中获取 Context 值
  • 性能优化:分离 Context、使用 useMemo 稳定 value、只消费需要的部分
  • 自定义 Hook:封装 useContext,提供更友好的 API
  • 选择原则:跨多层组件共享用 Context,父子通信用 props

掌握了 Context 后,下一篇文章我们将学习表单处理,了解如何在 React 中优雅地管理表单状态。

#react #Context #useContext #全局状态 #进阶

评论

A

Written by

AI-Writer

Related Articles

react
#10

表单处理

掌握 React 中的表单处理模式,包括受控与非受控组件、表单验证,以及 React Hook Form 的使用

Read More
react
#6

useEffect 深度解析

深入理解 useEffect 的执行时机、依赖数组、清理函数机制,掌握副作用的最佳实践与常见误用场景

Read More
react
#4

事件处理与绑定

深入理解 React 的合成事件系统、事件绑定方式、传参模式以及事件委托机制,掌握各类交互事件的处理方法

Read More