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"(属性钻取),代码啰嗦、难维护Layout 和 Sidebar 根本不关心 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>
);
}中间的 Layout、Sidebar 不用改任何代码——它们"透明"地让 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 全部封装在一个文件里,对外只暴露 ThemeProvider 和 useTheme。使用者不需要知道内部细节,就像用一个第三方库。
6. Context 的陷阱:性能
Context 不是银弹,有个性能坑要小心:
- Provider 的 value 变化时,所有消费它的组件都会重渲染——即使它们只用到了 value 的一小部分。
- 如果 value 是个对象
{ user, theme, cart },其中任一变化都会触发所有消费者重渲染。 - 对策:把不相关的数据拆成多个 Context;或用
useMemo包裹 value,避免每次渲染都创建新对象。
7. 何时用 Context,何时用状态管理库
- 用 Context:主题、语言、当前用户这种低频变化的全局数据。简单项目、中小应用首选。
- 用 Zustand / Redux Toolkit:购物车、聊天消息这种高频变化、逻辑复杂的状态。Context 的性能问题在这些场景会暴露。
- 经验:新手别一上来就堆 Redux,90% 的项目 useState + useContext 就够了。
小结
Context 解决 props 透传,三步走:创建(createContext)→ 提供(Provider)→ 消费(useContext)。封装自定义 Hook 加错误检查。适合主题、用户、语言等全局数据。下一篇看表单怎么处理——受控组件。
← 上一篇 列表渲染与 key
下一篇 受控组件与表单 →