Hook16.8+

useContext

读取和订阅 Context,实现跨组件状态共享

核心概述

在 React 中,数据通常通过 props 自上而下传递。但当深层组件需要顶层组件的数据时, 会导致Props 穿透(Prop Drilling)问题:中间层组件被迫接收并转发它们自己不使用的 props。 这不仅让代码冗长,还让组件耦合度变高,难以维护。

useContext 通过 Context 机制来解决这个问题。 它允许组件"跨越"中间层,直接订阅顶层组件提供的数据。当 context 值变化时, 所有订阅该 context 的组件都会自动重新渲染

useContext 适用于以下场景:

  • 避免 Props 穿透:深层组件需要访问全局或共享状态(如主题、用户信息)
  • 跨组件通信:不同层级的组件需要共享和更新同一份数据
  • 全局状态管理:配合 useReducer 实现轻量级状态管理(主题、国际化、认证状态)
  • 依赖注入:向组件树提供配置、服务实例或第三方库集成

💡 心智模型:隐形通道

将 Context 想象成组件树中的一条隐形通道:

  • Provider: 在组件树入口处"铺设"通道,放入数据
  • useContext: 组件通过"入口"进入通道,直接读取数据
  • 订阅机制: 当通道中的数据更新时,所有入口处都会收到通知

不同于 props 需要一层层传递,Context 让数据可以"直达"目标组件。 但要注意:通道中的数据变化会通知所有订阅者,导致它们全部重新渲染。

技术规格

类型签名

TypeScript
function useContext<T>(context: Context<T>): T

参数说明

参数类型说明
contextContext<T>createContext 创建的 context 对象

返回值

useContext 返回该 context 的当前值。这个值由以下规则决定:

  • 查找调用组件树中最近的 Context.Provider
  • 返回该 Provider 的 value prop
  • 如果没有找到 Provider,返回 createContext(defaultValue) 的默认值

运行机制

查找规则:

  • useContext 会向上遍历组件树,查找最近的匹配 Provider
  • 查找范围仅限于调用组件的 Provider,不会跨越组件边界
  • Provider 可以嵌套,内层 Provider 会覆盖外层 Provider 的值

订阅和更新:

  • React 自动订阅 context 值的变化
  • 当 Provider 的 value 变化时,所有使用该 context 的组件都会重新渲染
  • 使用 Object.is 比较新旧值,决定是否触发更新

性能影响:

  • 即使组件使用了 React.memo,context 值变化仍会导致重新渲染
  • 无法通过跳过子组件渲染来优化 context 更新
  • 推荐将变化频繁的状态和不变的状态拆分为不同的 context

实战演练

基础用法:主题切换

最常见的 Context 使用场景是提供全局主题配置。

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

// ============= 1. 定义 Context 类型 =============
type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  setTheme: (theme: Theme) => void;
}

// ============= 2. 创建 Context =============
const ThemeContext = createContext<ThemeContextType | null>(null);

// ============= 3. 创建自定义 Hook =============
function useTheme() {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }

  return context;
}

// ============= 4. 创建 Provider 组件 =============
import { useState } from 'react';

interface ThemeProviderProps {
  children: React.ReactNode;
  defaultTheme?: Theme;
}

export function ThemeProvider({ children, defaultTheme = 'light' }: ThemeProviderProps) {
  const [theme, setTheme] = useState<Theme>(defaultTheme);

  const value: ThemeContextType = {
    theme,
    setTheme,
  };

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

// ============= 5. 使用 Context 的组件 =============
function ThemeSwitcher() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
      className="px-4 py-2 rounded"
    >
      当前主题: {theme}
    </button>
  );
}

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

  return (
    <div className={`p-4 rounded ${theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-black'}`}>
      <h3>卡片内容</h3>
      <p>当前主题: {theme}</p>
    </div>
  );
}

// ============= 6. 应用入口 =============
export default function App() {
  return (
    <ThemeProvider defaultTheme="light">
      <div className="min-h-screen p-8">
        <ThemeSwitcher />
        <ThemedCard />
      </div>
    </ThemeProvider>
  );
}

生产级案例:全局状态管理(useContext + useReducer)

展示如何使用 Context + useReducer 实现完整的全局状态管理,包括用户认证、主题、国际化等。

TypeScript
import { createContext, useContext, useReducer, ReactNode, useMemo } from 'react';

// ============= 类型定义 =============
interface User {
  id: string;
  name: string;
  email: string;
}

interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  locale: 'zh-CN' | 'en';
  isLoading: boolean;
}

type AppAction =
  | { type: 'SET_USER'; payload: User | null }
  | { type: 'SET_THEME'; payload: 'light' | 'dark' }
  | { type: 'SET_LOCALE'; payload: 'zh-CN' | 'en' }
  | { type: 'SET_LOADING'; payload: boolean };

interface AppContextType {
  state: AppState;
  dispatch: React.Dispatch<AppAction>;
}

// ============= 创建 Context =============
const AppContext = createContext<AppContextType | null>(null);

// ============= Reducer =============
function appReducer(state: AppState, action: AppAction): AppState {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'SET_THEME':
      return { ...state, theme: action.payload };
    case 'SET_LOCALE':
      return { ...state, locale: action.payload };
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    default:
      return state;
  }
}

// ============= 初始状态 =============
const initialState: AppState = {
  user: null,
  theme: 'light',
  locale: 'zh-CN',
  isLoading: false,
};

// ============= Provider 组件 =============
interface AppProviderProps {
  children: ReactNode;
}

export function AppProvider({ children }: AppProviderProps) {
  const [state, dispatch] = useReducer(appReducer, initialState);

  // ✅ 使用 useMemo 优化,避免不必要的重新渲染
  const value = useMemo(() => ({ state, dispatch }), [state]);

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

// ============= 自定义 Hooks =============
export function useAppState() {
  const context = useContext(AppContext);

  if (!context) {
    throw new Error('useAppState must be used within AppProvider');
  }

  return context.state;
}

export function useDispatch() {
  const context = useContext(AppContext);

  if (!context) {
    throw new Error('useDispatch must be used within AppProvider');
  }

  return context.dispatch;
}

// ✅ 细粒度的自定义 Hooks,只订阅需要的状态
export function useUser() {
  const state = useAppState();
  return state.user;
}

export function useTheme() {
  const state = useAppState();
  const dispatch = useDispatch();

  return {
    theme: state.theme,
    setTheme: (theme: 'light' | 'dark') => {
      dispatch({ type: 'SET_THEME', payload: theme });
    },
  };
}

// ============= 业务 Hooks =============
export function useAuth() {
  const user = useUser();
  const dispatch = useDispatch();

  const login = async (email: string, password: string) => {
    dispatch({ type: 'SET_LOADING', payload: true });

    try {
      // 模拟 API 调用
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('登录失败');
      }

      const userData: User = await response.json();
      dispatch({ type: 'SET_USER', payload: userData });
    } catch (error) {
      console.error('Login error:', error);
      throw error;
    } finally {
      dispatch({ type: 'SET_LOADING', payload: false });
    }
  };

  const logout = () => {
    dispatch({ type: 'SET_USER', payload: null });
  };

  return {
    user,
    isAuthenticated: !!user,
    isLoading: useAppState().isLoading,
    login,
    logout,
  };
}

// ============= 使用示例 =============
function LoginForm() {
  const { login, isLoading } = useAuth();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const email = formData.get('email') as string;
    const password = formData.get('password') as string;

    try {
      await login(email, password);
      alert('登录成功!');
    } catch (error) {
      alert('登录失败,请检查用户名和密码');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
      <h2 className="text-2xl font-bold mb-6">登录</h2>

      <div className="mb-4">
        <label className="block text-sm font-medium mb-2">邮箱</label>
        <input
          type="email"
          name="email"
          required
          className="w-full px-3 py-2 border rounded"
          disabled={isLoading}
        />
      </div>

      <div className="mb-6">
        <label className="block text-sm font-medium mb-2">密码</label>
        <input
          type="password"
          name="password"
          required
          className="w-full px-3 py-2 border rounded"
          disabled={isLoading}
        />
      </div>

      <button
        type="submit"
        disabled={isLoading}
        className="w-full bg-blue-500 text-white py-2 rounded disabled:bg-gray-300"
      >
        {isLoading ? '登录中...' : '登录'}
      </button>
    </form>
  );
}

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

  if (!isAuthenticated) {
    return <p>请先登录</p>;
  }

  return (
    <div className="p-6 bg-white rounded-lg shadow">
      <h2 className="text-2xl font-bold mb-4">用户信息</h2>
      <p><strong>姓名:</strong> {user?.name}</p>
      <p><strong>邮箱:</strong> {user?.email}</p>

      <button
        onClick={logout}
        className="mt-4 px-4 py-2 bg-red-500 text-white rounded"
      >
        退出登录
      </button>
    </div>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
      className="px-4 py-2 bg-gray-200 rounded"
    >
      切换到 {theme === 'light' ? '深色' : '浅色'} 主题
    </button>
  );
}

// ============= 应用入口 =============
export default function App() {
  return (
    <AppProvider>
      <div className="min-h-screen bg-gray-50 p-8">
        <ThemeToggle />
        <div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-8">
          <LoginForm />
          <UserProfile />
        </div>
      </div>
    </AppProvider>
  );
}

关键点:这个示例展示了 Context + useReducer 的完整模式:

  • 使用 useReducer 集中管理状态逻辑
  • 使用 useMemo 优化 context value,避免不必要的重新渲染
  • 创建细粒度的自定义 Hooks(useUser, useTheme),只订阅需要的状态
  • 封装业务逻辑(useAuth),提供简洁的 API(login, logout)

避坑指南

❌ 陷阱 1: 每次 render 创建新的 value

如果在 Provider 组件中直接创建对象或数组作为 value,每次父组件渲染时都会创建新的引用, 导致所有消费该 context 的组件不必要地重新渲染

❌ 错误代码

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

  // ❌ 每次渲染都创建新对象!
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 即使 theme 没变,父组件重新渲染时,
// { theme, setTheme } 是新对象,
// 所有使用 useContext 的组件都会重新渲染

✅ 修正代码

TypeScript
import { useMemo } from 'react';

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

  // ✅ 使用 useMemo 缓存 value
  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme] // 只有 theme 变化时才创建新对象
  );

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

心智模型:React 使用 Object.is 比较新旧 context value。 如果每次渲染都是新对象,React 会认为值变了,触发所有订阅组件重新渲染。 使用 useMemo 缓存对象,只在依赖变化时才创建新对象。

❌ 陷阱 2: 所有状态放一个 Context

将所有应用状态放在一个 context 中会导致性能问题:任何一个状态变化都会导致所有消费该 context 的组件重新渲染,即使它们只使用了其中一部分状态。

❌ 错误代码

TypeScript
// ❌ 所有状态放在一个 context
interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  locale: string;
  notifications: Notification[];
  cart: CartItem[];
  // ... 更多状态
}

const AppContext = createContext<AppState | null>(null);

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

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

// 问题:当 cart 变化时,
// 只使用 theme 的组件也会重新渲染

✅ 修正代码

TypeScript
// ✅ 按变化频率拆分 context
const UserContext = createContext<UserContextType | null>(null);
const ThemeContext = createContext<ThemeContextType | null>(null);
const CartContext = createContext<CartContextType | null>(null);

function AppProvider({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <CartProvider>
          {children}
        </CartProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

// 优势:cart 变化不会影响使用 theme 的组件

❌ 陷阱 3: 组件在 Provider 外部使用 useContext

如果组件不在对应的 Provider 内部,useContext 会返回默认值(通常是 null 或 undefined), 导致运行时错误或难以调试的问题。

❌ 错误代码

TypeScript
function Button() {
  const { theme } = useContext(ThemeContext);
  // 如果不在 Provider 内,theme 可能是 undefined
  return <button className={theme}>点击</button>;
}

// ❌ 直接使用,可能出错
function App() {
  return <Button />; // 没有包裹 Provider!
}

✅ 修正代码

TypeScript
// ✅ 自定义 Hook 中检查
function useTheme() {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error(
      'useTheme must be used within ThemeProvider'
    );
  }

  return context;
}

function Button() {
  const { theme } = useTheme(); // 安全
  return <button className={theme}>点击</button>;
}

// ✅ 确保包裹 Provider
function App() {
  return (
    <ThemeProvider>
      <Button />
    </ThemeProvider>
  );
}

❌ 陷阱 4: 在条件语句或循环中使用 useContext

useContext 是 Hook,必须遵循 Hooks 规则:只能在组件顶层调用,不能在条件语句、循环或嵌套函数中调用。

❌ 错误代码

TypeScript
function Component({ showTheme }) {
  if (showTheme) {
    // ❌ 在条件语句中调用 Hook!
    const theme = useContext(ThemeContext);
    return <div>{theme}</div>;
  }

  return <div>No theme</div>;
}

function Component() {
  const items = [1, 2, 3];

  return items.map(item => {
    // ❌ 在循环(回调函数)中调用 Hook!
    const theme = useContext(ThemeContext);
    return <div key={item}>{theme}</div>;
  });
}

✅ 修正代码

TypeScript
// ✅ 始终在组件顶层调用
function Component({ showTheme }) {
  const theme = useContext(ThemeContext);

  if (showTheme) {
    return <div>{theme}</div>;
  }

  return <div>No theme</div>;
}

function Component() {
  const theme = useContext(ThemeContext);
  const items = [1, 2, 3];

  return items.map(item => (
    <div key={item}>{theme}</div>
  ));
}

最佳实践

✅ 1. 按变化频率拆分 Context

将频繁变化的状态和不变化或很少变化的状态拆分为不同的 context,避免不必要的重新渲染。

TypeScript
// ✅ 静态配置:很少变化
const ConfigContext = createContext({
  apiUrl: 'https://api.example.com',
  features: { featureA: true, featureB: false },
});

// ✅ 用户数据:登录后不变
const UserContext = createContext<User | null>(null);

// ✅ 主题:偶尔变化
const ThemeContext = createContext<Theme>('light');

// ✅ 购物车:频繁变化
const CartContext = createContext<CartContextType | null>(null);

✅ 2. 分离 State 和 Dispatch

对于使用 useReducer 的场景,将 state 和 dispatch 拆分为两个 context。 这样只读取 dispatch 的组件不会因为 state 变化而重新渲染。

TypeScript
// ❌ 所有组件都在意 state 变化
const AppContext = createContext({ state, dispatch });

// ✅ 拆分为两个 context
const AppStateContext = createContext<AppState | null>(null);
const AppDispatchContext = createContext<Dispatch<Action> | null>(null);

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState);

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

// ✅ 只需要 dispatch 的组件不会因 state 变化而重新渲染
function SaveButton() {
  const dispatch = useContext(AppDispatchContext);

  const handleClick = () => {
    dispatch({ type: 'SAVE' });
  };

  return <button onClick={handleClick}>保存</button>;
}

✅ 3. 创建自定义 Hook 封装

创建自定义 Hook 封装 useContext,提供类型安全和错误检查。

TypeScript
// ✅ 推荐模式:自定义 Hook + 类型安全
function useTheme() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error('useTheme must be used within ThemeProvider');
  }

  return context;
}

// 使用时更简洁,且有类型提示
function Button() {
  const { theme, setTheme } = useTheme();
  // ...
}

✅ 4. 使用 TypeScript 严格类型

避免使用 | null 作为 context 类型,而是创建两个版本的 context: 一个用于 Provider(包含值),一个用于消费者(确保值存在)。

TypeScript
// ✅ 创建不包含 null 的 context 类型
type ThemeContextType = {
  theme: 'light' | 'dark';
  setTheme: (theme: 'light' | 'dark') => void;
};

const ThemeContext = createContext<ThemeContextType | null>(null);

// ✅ 创建类型断言的 Hook
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }

  return context; // TypeScript 知道这里不会是 null
}

延伸阅读

这篇文章有帮助吗?