Context API #

一、Context概述 #

1.1 什么是Context #

Context 提供了一种在组件树中共享数据的方式,无需手动逐层传递 props。

1.2 使用场景 #

场景 说明
主题切换 全局主题样式
用户信息 登录状态、用户数据
语言设置 国际化
全局配置 应用配置信息

1.3 解决的问题 #

javascript
// ❌ Prop Drilling问题
function App() {
  const user = { name: 'Alice' };
  return <Header user={user} />;
}

function Header({ user }) {
  return <Navigation user={user} />;
}

function Navigation({ user }) {
  return <UserMenu user={user} />;
}

function UserMenu({ user }) {
  return <span>{user.name}</span>;
}

// ✅ 使用Context解决
function App() {
  return (
    <UserContext.Provider value={{ name: 'Alice' }}>
      <Header />
    </UserContext.Provider>
  );
}

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

二、基本使用 #

2.1 创建Context #

javascript
import { createContext } from 'react';

// 创建Context,可以设置默认值
const ThemeContext = createContext('light');
const UserContext = createContext(null);
const ConfigContext = createContext({ theme: 'light', lang: 'zh' });

2.2 Provider提供数据 #

javascript
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' });

  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={{ user, setUser }}>
        <Layout />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

2.3 消费Context #

javascript
import { useContext } from 'react';

function ThemedButton() {
  const theme = useContext(ThemeContext);
  
  return (
    <button className={`btn btn-${theme}`}>
      Themed Button
    </button>
  );
}

function UserProfile() {
  const { user, setUser } = useContext(UserContext);

  return (
    <div>
      <span>{user.name}</span>
      <button onClick={() => setUser(null)}>退出</button>
    </div>
  );
}

三、Context最佳实践 #

3.1 创建Context文件 #

javascript
// contexts/ThemeContext.jsx
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(undefined);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

  return (
    <ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

3.2 使用自定义Provider #

javascript
import { ThemeProvider } from './contexts/ThemeContext';
import { UserProvider } from './contexts/UserContext';

function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <Layout />
      </UserProvider>
    </ThemeProvider>
  );
}

function ThemedComponent() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <div className={theme}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

3.3 分离Context #

javascript
// 分离状态和dispatch,优化性能
const UserStateContext = createContext();
const UserDispatchContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  return (
    <UserStateContext.Provider value={user}>
      <UserDispatchContext.Provider value={setUser}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
}

export function useUserState() {
  const context = useContext(UserStateContext);
  if (context === undefined) {
    throw new Error('useUserState must be used within UserProvider');
  }
  return context;
}

export function useUserDispatch() {
  const context = useContext(UserDispatchContext);
  if (context === undefined) {
    throw new Error('useUserDispatch must be used within UserProvider');
  }
  return context;
}

四、完整示例 #

4.1 用户认证Context #

javascript
// contexts/AuthContext.jsx
import { createContext, useContext, useState, useCallback } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
  });

  const login = useCallback(async (credentials) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    });
    const data = await response.json();
    setUser(data.user);
    localStorage.setItem('user', JSON.stringify(data.user));
    return data;
  }, []);

  const logout = useCallback(() => {
    setUser(null);
    localStorage.removeItem('user');
  }, []);

  const value = {
    user,
    login,
    logout,
    isAuthenticated: !!user
  };

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

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

4.2 使用认证Context #

javascript
function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route
            path="/dashboard"
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            }
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

function ProtectedRoute({ children }) {
  const { isAuthenticated } = useAuth();
  
  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }
  
  return children;
}

function Login() {
  const { login } = useAuth();
  const navigate = useNavigate();

  const handleSubmit = async (credentials) => {
    await login(credentials);
    navigate('/dashboard');
  };

  return <LoginForm onSubmit={handleSubmit} />;
}

function Header() {
  const { user, logout } = useAuth();

  return (
    <header>
      <span>{user?.name}</span>
      <button onClick={logout}>退出</button>
    </header>
  );
}

4.3 购物车Context #

javascript
// contexts/CartContext.jsx
import { createContext, useContext, useReducer, useCallback } from 'react';

const CartContext = createContext(null);

const initialState = {
  items: [],
  total: 0
};

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      const existingIndex = state.items.findIndex(
        item => item.id === action.payload.id
      );
      
      if (existingIndex >= 0) {
        const items = [...state.items];
        items[existingIndex] = {
          ...items[existingIndex],
          quantity: items[existingIndex].quantity + 1
        };
        return {
          items,
          total: state.total + action.payload.price
        };
      }
      
      return {
        items: [...state.items, { ...action.payload, quantity: 1 }],
        total: state.total + action.payload.price
      };

    case 'REMOVE_ITEM':
      const item = state.items.find(i => i.id === action.payload);
      return {
        items: state.items.filter(i => i.id !== action.payload),
        total: state.total - (item.price * item.quantity)
      };

    case 'UPDATE_QUANTITY':
      const { id, quantity } = action.payload;
      const targetItem = state.items.find(i => i.id === id);
      const diff = (quantity - targetItem.quantity) * targetItem.price;
      
      return {
        items: state.items.map(i =>
          i.id === id ? { ...i, quantity } : i
        ),
        total: state.total + diff
      };

    case 'CLEAR_CART':
      return initialState;

    default:
      return state;
  }
}

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

  const addItem = useCallback((item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  }, []);

  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' });
  }, []);

  return (
    <CartContext.Provider
      value={{
        ...state,
        addItem,
        removeItem,
        updateQuantity,
        clearCart
      }}
    >
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within CartProvider');
  }
  return context;
}

五、性能优化 #

5.1 避免不必要的渲染 #

javascript
// 分离频繁变化和不频繁变化的数据
const UserContext = createContext();
const UserActionsContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const actions = useMemo(() => ({
    login: async (credentials) => { /* ... */ },
    logout: () => setUser(null),
    updateProfile: (data) => setUser(prev => ({ ...prev, ...data }))
  }), []);

  return (
    <UserContext.Provider value={user}>
      <UserActionsContext.Provider value={actions}>
        {children}
      </UserActionsContext.Provider>
    </UserContext.Provider>
  );
}

// 只使用actions的组件不会因为user变化而重新渲染
function LoginButton() {
  const { login } = useContext(UserActionsContext);
  return <button onClick={() => login()}>登录</button>;
}

5.2 使用useMemo优化 #

javascript
function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const value = useMemo(() => ({
    user,
    setUser,
    isAuthenticated: !!user
  }), [user]);

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

5.3 拆分Context #

javascript
// 将大型Context拆分为多个小型Context
function App() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <CartProvider>
          <Layout />
        </CartProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

六、常见问题 #

6.1 默认值的使用 #

javascript
// 默认值用于没有匹配Provider时
const ThemeContext = createContext('light');

// 组件在Provider外部使用时获得默认值
function Button() {
  const theme = useContext(ThemeContext); // 'light'
  return <button className={theme}>Button</button>;
}

// 建议:使用undefined作为默认值,便于检测
const ThemeContext = createContext(undefined);

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

6.2 Context嵌套过深 #

javascript
// ❌ 嵌套过深
<Provider1>
  <Provider2>
    <Provider3>
      <Provider4>
        <App />
      </Provider4>
    </Provider3>
  </Provider2>
</Provider1>

// ✅ 组合Provider
function composeProviders(...providers) {
  return providers.reduce((Prev, Provider) => {
    return ({ children }) => (
      <Prev>
        <Provider>{children}</Provider>
      </Prev>
    );
  });
}

const AppProvider = composeProviders(
  ThemeProvider,
  AuthProvider,
  CartProvider
);

function App() {
  return (
    <AppProvider>
      <Layout />
    </AppProvider>
  );
}

七、总结 #

要点 说明
createContext 创建Context
Provider 提供数据
useContext 消费数据
自定义Hook 封装Context访问

核心原则:

  • 使用 Context 避免 prop drilling
  • 封装 Provider 和自定义 Hook
  • 分离状态和操作优化性能
  • 提供有意义的错误提示
最后更新:2026-03-26