Context 跨层传递

props 是父子通信的好工具,但当数据要跨越五六层组件时,中间的组件只是机械地"透传" props,代码又臭又长——这叫 prop drilling(属性钻取)。React 提供了 Context 来解决这个问题:让数据"跳过"中间层,直接送到需要的地方。

1. 痛点:prop drilling

看一个典型场景:用户登录信息要在很深的组件里显示。

// 痛点:props 要传很多层,中间组件只是"过路"
function App() {
  const [user, setUser] = useState({ name: "小明" });
  return <Layout user={user} />;       // 第 1 层
}

function Layout({ user }) {
  return <Sidebar user={user} />;       // 第 2 层(只是透传)
}

function Sidebar({ user }) {
  return <UserInfo user={user} />;      // 第 3 层(只是透传)
}

function UserInfo({ user }) {
  return <p>{user.name}</p>;            // 第 4 层才真正用
}
// 这种叫 "prop drilling"(属性钻取),代码啰嗦、难维护

LayoutSidebar 根本不关心 user,只是被迫接收并透传。如果以后 user 的结构变了,要改四个文件;如果再加一个语言、主题,又要再钻一遍。这种代码维护起来很痛苦。

2. Context 三步走

Context 的用法固定三步:创建 → 提供 → 消费

第一步:创建 Context

// 1. 创建 Context(通常放在单独的文件里)
import { createContext } from "react";
export const UserContext = createContext(null);
// 参数是"默认值":没有 Provider 包裹时使用,通常是 null 或 undefined

第二步:用 Provider 提供数据

import { UserContext } from "./UserContext";

// 2. 在上层组件用 Provider 提供数据
function App() {
  const [user, setUser] = useState({ name: "小明", role: "admin" });

  // Provider 的 value 属性就是要共享的数据
  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

Provider 包裹住的所有子组件(无论多深),都能读到 value 里的数据。Provider 的 value 变化时,所有消费它的组件会自动重渲染。

第三步:用 useContext 消费

import { useContext } from "react";
import { UserContext } from "./UserContext";

// 3. 在任意下层组件用 useContext 读取
function UserInfo() {
  const user = useContext(UserContext);   // 直接拿到,不用 props
  return <p>当前用户:{user.name} ({user.role})</p>;
}

// 哪怕隔了十层,中间组件不需要任何改动
function DeepComponent() {
  return (
    <div>
      <VeryDeep />
    </div>
  );
}

中间的 LayoutSidebar 不用改任何代码——它们"透明"地让 Context 数据穿过。这就是 Context 的魔力。

3. 经典应用:主题切换

Context 最常见的用途是全局共享状态:主题、当前语言、当前登录用户、购物车。下面是一个完整的主题切换示例:

// 经典应用:全局主题切换
import { createContext, useContext, useState } from "react";

const ThemeContext = createContext("light");

function App() {
  const [theme, setTheme] = useState("light");
  const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={toggle}>切换主题</button>
      <Page />
    </ThemeContext.Provider>
  );
}

function Page() {
  const theme = useContext(ThemeContext);
  const isDark = theme === "dark";
  return (
    <div style={{
      background: isDark ? "#222" : "#fff",
      color: isDark ? "#fff" : "#000",
      padding: "20px"
    }}>
      当前主题:{theme}
    </div>
  );
}

在 App 顶层包一个 Provider,任何子组件都能读主题、改主题。这种"全局状态"的写法比 Redux 简单得多,中小项目首选。

4. 最佳实践:封装自定义 Hook

直接用 useContext 有个小问题:如果忘了在 Provider 内用,会拿到 null 或默认值,bug 难排查。推荐封装一个自定义 Hook:

// 最佳实践:封装一个自定义 Hook,加错误检查
import { useContext } from "react";
import { UserContext } from "./UserContext";

export function useUser() {
  const user = useContext(UserContext);
  if (user === null) {
    throw new Error("useUser 必须在 UserContext.Provider 内部使用");
  }
  return user;
}

// 用法:其他组件调 const user = useUser();
// 忘了包 Provider 会立刻报错,而不是出现诡制的 undefined bug

这样使用方调 useUser(),如果没包 Provider 会立刻报错,定位问题超快。这是 React 社区的标准做法。

5. 推荐的文件组织

// 推荐模式:Context + 自定义 Hook + 状态逻辑 封装
// themes/ThemeProvider.jsx
const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  const toggle = () => setTheme(t => (t === "light" ? "dark" : "light"));
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme 必须在 ThemeProvider 内");
  return ctx;
}

// 在 App 顶层包裹
<ThemeProvider>
  <App />
</ThemeProvider>

这种模式叫 "Context + Hook"封装:把 Context 创建、Provider、状态逻辑、自定义 Hook 全部封装在一个文件里,对外只暴露 ThemeProvideruseTheme。使用者不需要知道内部细节,就像用一个第三方库。

6. Context 的陷阱:性能

Context 不是银弹,有个性能坑要小心:

7. 何时用 Context,何时用状态管理库

小结

Context 解决 props 透传,三步走:创建(createContext)→ 提供(Provider)→ 消费(useContext)。封装自定义 Hook 加错误检查。适合主题、用户、语言等全局数据。下一篇看表单怎么处理——受控组件

← 上一篇 列表渲染与 key

下一篇 受控组件与表单

✈️💬