目 录CONTENT

文章目录

React(十二)Context 完全指南

React(十二):Context 完全指南 —— 跨组件数据共享的正确姿势

适用版本:React 19
前置知识:函数组件、useState、TypeScript 基础
学习目标:掌握 Context 的核心机制、完整实现流程、React 19 新特性、常见坑点与最佳实践


目录

  1. 为什么需要 Context:从 props 透传之痛说起
  2. React 19 Context 完整实现流程
  3. 新手常见错误与正确写法
  4. 注意事项
  5. 实战案例:主题切换系统
  6. 课后思考题

一、为什么需要 Context:从 props 透传之痛说起

1.1 问题:props 逐层透传(prop drilling)

假设我们有一个 App 组件,要把当前登录用户信息传给最底层的 Avatar 组件。中间经过 HeaderUserMenu 两层组件。

不使用 Context 的写法

// App.tsx:源头拥有数据
function App() {
  const [user] = useState({
    id: 1,
    name: "张三",
    avatar: "https://example.com/avatar.jpg",
  });

  return <Layout user={user} />;
}

// Layout.tsx:不需要 user,但必须帮忙往下传
function Layout({ user }: { user: User }) {
  return (
    <div className="layout">
      <Header user={user} />
      <main>主要内容区域</main>
    </div>
  );
}

// Header.tsx:也不需要 user,继续往下传
function Header({ user }: { user: User }) {
  return (
    <header>
      <Logo />
      <UserMenu user={user} />
    </header>
  );
}

// UserMenu.tsx:接近目标了,继续传
function UserMenu({ user }: { user: User }) {
  return (
    <div className="user-menu">
      <span>{user.name}</span>
      <Avatar user={user} />
    </div>
  );
}

// Avatar.tsx:真正需要 user 的地方
function Avatar({ user }: { user: User }) {
  return <img src={user.avatar} alt={user.name} className="avatar" />;
}

痛点分析

痛点 说明
中间组件被迫写样板代码 LayoutHeader 根本不需要 user,但为了传给子组件必须接收再转发
耦合度提高 中间组件的 props 签名与最终消费者强行绑定
难以维护 如果有一天 UserMenu 改成兄弟组件共享,需要改动的层级很多
命名污染 每一层都要起 user 这个 prop 名字,容易和本层自己的状态混淆

1.2 Context 的解决价值

Context 提供了一种跨组件直接共享数据的能力,让数据生产者(Provider)和消费者可以直接"隔空对话",无需中间组件层层转发。

// 使用 Context 后,Layout 和 Header 完全不需要知道 user 的存在
function App() {
  return (
    <UserProvider>
      <Layout />
    </UserProvider>
  );
}

function Layout() {
  return (
    <div className="layout">
      <Header />
      <main>主要内容区域</main>
    </div>
  );
}

function Header() {
  return (
    <header>
      <Logo />
      <UserMenu />
    </header>
  );
}

function UserMenu() {
  return (
    <div className="user-menu">
      <UserName />
      <Avatar />
    </div>
  );
}

一句话总结:Context 是 React 提供的"跨层级广播"机制,让共享数据像 radio 信号一样,谁需要谁接收。

1.3 Context 适合与不适合的场景

✅ 适合使用 Context 的场景

场景 说明 示例
全局用户状态 登录态、用户信息、权限角色 当前登录用户、token
主题配置 亮色/暗色模式、颜色变量、字体大小 全局主题切换
多语言设置 当前语言、翻译函数 i18n 切换
全局 UI 状态 弹窗、通知、toast、侧边栏展开 全局消息通知中心
配置类信息 不需要频繁变化的 app 配置 站点名称、API 基础地址

❌ 不适合使用 Context 的场景

场景 原因 推荐方案
父子组件之间简单传参 杀鸡用牛刀,引入复杂度 直接使用 props
高频变化的状态 Provider 的 value 变化会导致所有消费者重渲染 使用状态管理库(Zustand、Jotai、Redux Toolkit)
组件复用性要求高的数据 Context 让组件依赖外部上下文,难以单独复用 通过 props 传入或组合组件
仅用于避免少量 props 传递 层数不多的情况下 props 更清晰 props 透传

二、React 19 Context 完整实现流程

2.1 创建 Context

Context 创建使用 React.createContext() 方法。React 19 中这个方法与之前版本保持一致,但结合 TypeScript 使用更规范。

基本语法

const MyContext = createContext(defaultValue);
参数 说明
defaultValue 当组件没有匹配到任何 Provider 时使用的默认值
返回值 一个 Context 对象,包含 ProviderConsumer 两个属性

示例:创建 ThemeContext(带 TypeScript)

import { createContext } from "react";

// 1. 定义数据类型
type Theme = "light" | "dark";

interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
}

// 2. 创建 Context,传入默认值
// 默认值会在组件没有匹配到 Provider 时生效
const ThemeContext = createContext<ThemeContextValue>({
  theme: "light", // 默认主题
  toggleTheme: () => {}, // 默认空函数,避免调用时报错
});

export default ThemeContext;

默认值(defaultValue)的 fallback 逻辑

// 场景:某组件被直接使用,没有被 Provider 包裹
function SomeComponent() {
  const { theme } = useContext(ThemeContext);
  return <div>当前主题:{theme}</div>; // 输出:当前主题:light
}

重要认知:默认值只有在没有 Provider 包裹时才会生效。如果 Provider 的 valueundefined,默认值也不会生效——React 会认为你显式传了 undefined

最佳实践:把 undefined 也纳入类型考虑

// 如果你希望消费者必须被 Provider 包裹,可以使用 null 作为默认值
const ThemeContext = createContext<ThemeContextValue | null>(null);

// 消费时强制判断
function SomeComponent() {
  const value = useContext(ThemeContext);

  if (value === null) {
    throw new Error("SomeComponent 必须在 ThemeProvider 内部使用");
  }

  return <div>当前主题:{value.theme}</div>;
}

2.2 配置 Provider 共享状态

Provider 基础用法

Context.Provider 是一个特殊的 React 组件,它的 value 属性就是你要共享给后代组件的数据。

import { useState } from "react";
import ThemeContext from "./ThemeContext";

function ThemeProvider({ children }: { children: React.ReactNode }) {
  // 使用 useState 管理共享状态
  const [theme, setTheme] = useState<"light" | "dark">("light");

  // 把状态和操作方法一起放入 value
  const value = {
    theme,
    toggleTheme: () => {
      setTheme((prev) => (prev === "light" ? "dark" : "light"));
    },
  };

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

export default ThemeProvider;

在根组件中包裹 Provider

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import ThemeProvider from "./ThemeProvider";

const root = ReactDOM.createRoot(document.getElementById("root")!);

root.render(
  // React 19 严格模式下建议包裹 Provider 在应用顶层
  <React.StrictMode>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </React.StrictMode>,
);

使用 useReducer 管理更复杂的状态

当 Context 共享的状态逻辑变复杂时,推荐用 useReducer 替代 useState

import { useReducer, createContext, ReactNode } from "react";

// 定义 State
type Theme = "light" | "dark";

interface ThemeState {
  theme: Theme;
  fontSize: "small" | "medium" | "large";
}

// 定义 Action 类型
type ThemeAction =
  | { type: "TOGGLE_THEME" }
  | { type: "SET_FONT_SIZE"; payload: ThemeState["fontSize"] };

// reducer 函数
function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
  switch (action.type) {
    case "TOGGLE_THEME":
      return { ...state, theme: state.theme === "light" ? "dark" : "light" };
    case "SET_FONT_SIZE":
      return { ...state, fontSize: action.payload };
    default:
      return state;
  }
}

interface ThemeContextValue {
  state: ThemeState;
  dispatch: React.Dispatch<ThemeAction>;
}

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

function ThemeProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(themeReducer, {
    theme: "light",
    fontSize: "medium",
  });

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

export { ThemeContext, ThemeProvider };

React 19 中 Provider 的渲染优化规则

在 React 19 中,Context Provider 的 value 仍然遵循引用相等的比较规则:

  • 如果 Provider 的 value 引用发生变化,所有订阅该 Context 的后代组件都会重新渲染
  • 如果 value 引用不变,即使内部某个属性变化,后代组件也不会重渲染(因为 React 只比较 value 的整体引用)

这意味着下面这种写法会导致每次 Provider 渲染都触发所有消费者重渲染

// ❌ 错误:每次渲染都创建新对象
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider
      value={{
        theme,
        toggleTheme: () => setTheme((t) => (t === "light" ? "dark" : "light")),
      }}
    >
      {children}
    </ThemeContext.Provider>
  );
}

✅ 正确写法:用 useMemo 缓存 value

import { useState, useMemo, ReactNode } from "react";
import ThemeContext from "./ThemeContext";

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  // 使用 useMemo 缓存 value 对象,只有当 theme 变化时才创建新对象
  const value = useMemo(
    () => ({
      theme,
      toggleTheme: () => setTheme((t) => (t === "light" ? "dark" : "light")),
    }),
    [theme],
  );

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

export default ThemeProvider;

React 19 说明:虽然 React 19 引入了更智能的编译优化,但 Context 的 value 比较仍然基于引用相等。对于高频使用的 Context,务必用 useMemo 缓存 value


2.3 三种消费 Context 的方式

React 提供了三种消费 Context 的方式,分别适用于不同场景。

方式一:useContext Hook(最常用,函数组件首选)

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

function ThemeButton() {
  // 直接读取 Context 中的 theme 和 toggleTheme
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === "dark" ? "#333" : "#fff",
        color: theme === "dark" ? "#fff" : "#333",
      }}
    >
      切换到 {theme === "dark" ? "浅色" : "深色"} 模式
    </button>
  );
}

export default ThemeButton;

适用场景:函数组件、代码简洁、现代 React 开发的标准写法。

方式二:Context.Consumer(类组件或渲染属性模式)

import ThemeContext from "./ThemeContext";

function ThemePanel() {
  return (
    <ThemeContext.Consumer>
      {(value) => (
        <div
          style={{
            backgroundColor: value.theme === "dark" ? "#222" : "#f5f5f5",
            color: value.theme === "dark" ? "#fff" : "#000",
            padding: 20,
          }}
        >
          <h3>当前主题:{value.theme}</h3>
          <button onClick={value.toggleTheme}>切换主题</button>
        </div>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemePanel;

适用场景

  • 类组件(不想用 contextType 的静态属性)
  • 需要在 JSX 中根据 Context 值进行条件渲染的片段
  • 渲染属性(render prop)模式遗留代码

方式三:contextType(类组件专用,限制较多)

import { Component } from "react";
import ThemeContext from "./ThemeContext";

class ThemeDisplay extends Component {
  // 在类组件中声明 contextType
  static contextType = ThemeContext;

  render() {
    // 通过 this.context 访问
    const { theme } = this.context;

    return (
      <div>
        类组件读取到的主题:<strong>{theme}</strong>
      </div>
    );
  }
}

export default ThemeDisplay;

限制

  • 一个类组件只能声明一个 contextType
  • 如果组件需要消费多个 Context,无法使用 contextType
  • 只能访问 Context,不能监听多个 Context

三种方式对比总结

消费方式 适用组件 可消费多个 Context 代码简洁度 推荐度
useContext 函数组件 ✅ 可以 ⭐⭐⭐ 首选
Context.Consumer 函数组件/类组件 ✅ 可以 ⭐⭐ 兼容旧代码
contextType 类组件 ❌ 只能一个 ⭐ 不推荐新项目

2.4 React 19 中的 Context 新特性

React 19 在 Context 方面没有翻天覆地的 API 变化,但有几个值得教学和开发关注的点:

1. Context 与新的 Hooks 体系兼容

React 19 引入了 useuseActionStateuseFormStatus 等新 Hooks,Context 与这些新 Hook 可以自然组合使用。例如用 use 读取 Context 在 Suspense 场景下更友好:

import { use, Suspense } from "react";
import ThemeContext from "./ThemeContext";

function ThemeAwareContent() {
  // use 可以读取 Context(在 React 19 中可用)
  const { theme } = use(ThemeContext);

  return <div>当前主题:{theme}</div>;
}

function App() {
  return (
    <ThemeProvider>
      <Suspense fallback={<div>加载中...</div>}>
        <ThemeAwareContent />
      </Suspense>
    </ThemeProvider>
  );
}

注意:use 目前仍处于逐步推广阶段,函数组件中 useContext 仍然是主流写法。

2. Provider 与渲染优化的边界

React 19 的编译器(React Compiler)可以自动缓存部分依赖,但 Context 的 value 仍然是引用比较。教学时需要反复强调:

  • 不要把 Context 当作"全局状态管理库"随意塞入高频变化的数据
  • 务必useMemo 包裹 value
  • 考虑把高频变化的数据放到更细粒度的 Context 中,或使用外部状态管理库

3. Server Components 与 Context 的兼容性

React 19 强化了 Server Components 能力。需要注意:

  • Context 是客户端概念,在 Server Component 中不能使用 useContext
  • 如果需要在 Server Component 中共享数据,应通过 props 传递
  • Context Provider 本身应放在客户端组件树中
// 客户端组件(Client Component)中才能使用 Context
"use client";

import { ThemeProvider } from "./ThemeProvider";

export function ClientRoot({ children }: { children: React.ReactNode }) {
  return <ThemeProvider>{children}</ThemeProvider>;
}

4. 与旧版 React 的差异

特性 React 18 及以前 React 19
createContext 支持 支持,行为一致
useContext 支持 支持,推荐方式
use 读取 Context 不支持 支持
Provider 引用比较 引用比较 引用比较(未改变)
Server Components 实验性 更成熟,Context 仍仅限客户端

三、新手常见错误与正确写法

3.1 错误:直接修改 Context value

❌ 错误示例

function BadComponent() {
  const { theme, setTheme } = useContext(ThemeContext);

  const handleClick = () => {
    // ❌ 错误:直接修改 Context 中的值
    theme = "dark"; // 这不会触发任何更新,且 TypeScript 可能报错
  };

  return <button onClick={handleClick}>切换</button>;
}

错误原因:Context 的值是只读的共享引用。直接修改它既不会触发 React 重新渲染,也会破坏单向数据流。

✅ 正确写法

function GoodComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  const handleClick = () => {
    // ✅ 正确:通过 Provider 提供的函数来修改状态
    toggleTheme();
  };

  return <button onClick={handleClick}>当前主题:{theme}</button>;
}

3.2 错误:Provider 嵌套层级错误导致取值异常

❌ 错误示例

function App() {
  return (
    <div>
      <UserProvider>
        <Header />
      </UserProvider>

      {/* 错误:Main 在 UserProvider 外部,无法消费 user */}
      <Main />
    </div>
  );
}

function Main() {
  const { user } = useContext(UserContext); // 得到的是默认值 null
  return <div>{user.name}</div>; // 报错!Cannot read properties of null
}

✅ 正确写法

function App() {
  return (
    <UserProvider>
      <div>
        <Header />
        <Main /> {/* ✅ Main 在 Provider 内部 */}
        <Footer />
      </div>
    </UserProvider>
  );
}

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

调试技巧:如果消费到的是默认值,首先检查组件是否真的被对应的 Provider 包裹。


3.3 错误:useContext 依赖错误导致重复渲染

❌ 错误示例

function UserProfile() {
  const user = useContext(UserContext); // 把整个 user 对象取出来

  // 假设 UserProfile 只关心 username
  return <div>用户名:{user.username}</div>;
}

上面的代码本身没问题,但如果 Provider 的 value 每次都创建新对象,即使 username 没变,也会引起 UserProfile 重渲染。

✅ 正确写法:拆分细粒度 Context

// 把用户信息拆成两个 Context
const UserIdentityContext = createContext(null); // 包含 username, id
const UserPreferencesContext = createContext(null); // 包含 theme, language

function UserProfile() {
  // 只消费自己关心的 Context
  const { username } = useContext(UserIdentityContext);
  return <div>用户名:{username}</div>;
}

function SettingsPanel() {
  // 只消费偏好设置 Context
  const { language } = useContext(UserPreferencesContext);
  return <div>语言:{language}</div>;
}

3.4 错误:Provider 的 value 没有使用 useMemo

// ❌ 每次 ThemeProvider 渲染都会创建新对象,导致所有消费者重渲染
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

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

✅ 正确写法

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

  const value = useMemo(() => ({ theme, setTheme }), [theme]);

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

四、注意事项

4.1 value 属性引用变化导致的不必要重渲染

问题:Provider 的 value 每次渲染都创建新对象,导致所有消费者都重新渲染。

解决方案

import { useMemo, useState } from "react";
import UserContext from "./UserContext";

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

  // 使用 useMemo 缓存 value
  const value = useMemo(
    () => ({
      user,
      isLogin,
      setUser,
      setIsLogin,
    }),
    [user, isLogin],
  );

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

4.2 拆分多个细粒度 Context

问题:把所有共享数据塞进一个 Context,导致任何小变化都引起全局重渲染。

方案:按关注点拆分。

// AuthContext.tsx
const AuthContext = createContext(null);

// ThemeContext.tsx
const ThemeContext = createContext(null);

// LocaleContext.tsx
const LocaleContext = createContext(null);

// 组合 Provider
function AppProviders({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LocaleProvider>{children}</LocaleProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

4.3 避免过度使用 Context

场景 推荐方案
父子两层传递 props
需要共享但变化不频繁 Context
高频变化或复杂状态 Zustand / Jotai / Redux Toolkit
组件内部状态 useState / useReducer

4.4 Context 默认值的调试技巧

// 开发环境下打印默认值命中情况
function useTheme() {
  const context = useContext(ThemeContext);

  if (context === null) {
    console.warn("useTheme 必须在 ThemeProvider 内部使用");
    throw new Error("useTheme 必须在 ThemeProvider 内部使用");
  }

  return context;
}

4.5 生产环境下的类型安全检查

// 推荐封装一个自定义 Hook,集中做类型检查
import { useContext } from "react";
import ThemeContext from "./ThemeContext";

export function useTheme() {
  const context = useContext(ThemeContext);

  if (context === null || context === undefined) {
    throw new Error("useTheme 必须在 ThemeProvider 内部使用");
  }

  return context;
}

4.6 动态创建 Context 的注意事项

// ❌ 错误:在组件内部动态创建 Context
function App() {
  const MyContext = createContext(null); // 每次渲染都会创建新 Context
  return ...
}

// ✅ 正确:在模块顶层创建 Context
const MyContext = createContext(null);

function App() {
  return ...
}

4.7 多级 Provider 嵌套时的值覆盖规则

Context 遵循"就近原则":组件会消费最近的匹配 Provider 的 value。

function App() {
  return (
    <ThemeProvider theme="light">
      <Parent />
    </ThemeProvider>
  );
}

function Parent() {
  return (
    <ThemeProvider theme="dark">
      <Child /> {/* Child 消费到的是 dark */}
    </ThemeProvider>
  );
}

function Child() {
  const { theme } = useTheme();
  return <div>当前主题:{theme}</div>; // 输出:dark
}

五、实战案例:主题切换系统

案例目标

实现一个完整的主题切换功能:

  • 创建 ThemeContext
  • 在根组件配置 ThemeProvider
  • 在嵌套 3 层的子组件中消费并切换主题

项目结构

src/
├── contexts/
│   ├── ThemeContext.tsx
│   └── ThemeProvider.tsx
├── components/
│   ├── Header.tsx
│   ├── NavBar.tsx
│   └── ThemeToggleButton.tsx
├── hooks/
│   └── useTheme.ts
├── App.tsx
└── main.tsx

步骤 1:创建 ThemeContext

// src/contexts/ThemeContext.tsx
import { createContext } from "react";

export type Theme = "light" | "dark";

export interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
}

// 使用 null 作为默认值,强制消费者必须被 Provider 包裹
export const ThemeContext = createContext<ThemeContextValue | null>(null);

步骤 2:创建 ThemeProvider

// src/contexts/ThemeProvider.tsx
import { useState, useMemo, ReactNode } from "react";
import { ThemeContext, Theme } from "./ThemeContext";

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

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

  // 使用 useMemo 缓存 value,避免不必要的重渲染
  const value = useMemo(
    () => ({
      theme,
      toggleTheme: () => {
        setTheme((prev) => (prev === "light" ? "dark" : "light"));
      },
    }),
    [theme],
  );

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

步骤 3:封装 useTheme Hook

// src/hooks/useTheme.ts
import { useContext } from "react";
import { ThemeContext } from "../contexts/ThemeContext";

export function useTheme() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error("useTheme 必须在 ThemeProvider 内部使用");
  }

  return context;
}

步骤 4:在 main.tsx 中包裹 Provider

// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ThemeProvider } from "./contexts/ThemeProvider";

const root = ReactDOM.createRoot(document.getElementById("root")!);

root.render(
  <React.StrictMode>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </React.StrictMode>,
);

步骤 5:在 App 中嵌套 3 层组件

// src/App.tsx
import Header from "./components/Header";

function App() {
  return (
    <div className="app">
      <Header />
    </div>
  );
}

export default App;
// src/components/Header.tsx
import NavBar from "./NavBar";

export default function Header() {
  return (
    <header className="header">
      <h1>我的应用</h1>
      <NavBar />
    </header>
  );
}
// src/components/NavBar.tsx
import ThemeToggleButton from "./ThemeToggleButton";

export default function NavBar() {
  return (
    <nav className="nav">
      <a href="#">首页</a>
      <a href="#">关于</a>
      <ThemeToggleButton />
    </nav>
  );
}

步骤 6:在底层组件消费并切换主题

// src/components/ThemeToggleButton.tsx
import { useTheme } from "../hooks/useTheme";

export default function ThemeToggleButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      className={`theme-toggle ${theme}`}
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === "dark" ? "#fff" : "#333",
        color: theme === "dark" ? "#333" : "#fff",
        padding: "8px 16px",
        border: "none",
        borderRadius: 4,
        cursor: "pointer",
      }}
    >
      {theme === "dark" ? "☀️ 切换到浅色" : "🌙 切换到深色"}
    </button>
  );
}

步骤 7:主题样式应用(可选)

// src/App.tsx
import { useTheme } from "./hooks/useTheme";
import Header from "./components/Header";

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

  return (
    <div
      className="app"
      style={{
        minHeight: "100vh",
        backgroundColor: theme === "dark" ? "#1a1a1a" : "#ffffff",
        color: theme === "dark" ? "#ffffff" : "#1a1a1a",
        transition: "all 0.3s ease",
      }}
    >
      <Header />
      <main style={{ padding: 20 }}>
        <p>当前主题:{theme}</p>
      </main>
    </div>
  );
}

export default App;

数据流图解

main.tsx
  └── ThemeProvider (value: { theme, toggleTheme })
        └── App
              └── Header
                    └── NavBar
                          └── ThemeToggleButton
                                └── useTheme() → 读取/切换 theme

数据从 ThemeProvider 向下广播,经过 AppHeaderNavBar,最终到达 ThemeToggleButton。中间组件完全不需要传递任何 props。


六、课后思考题

思考题 1

ThemeProvider 中,如果不使用 useMemo 包裹 value,当 ThemeProvider 的父组件重新渲染时,会发生什么?请解释原因。


思考题 2

如果一个类组件需要同时消费 ThemeContextUserContext,能否使用 contextType?如果不能,应该使用哪种方式?


思考题 3

在实际项目中,如果用户偏好设置(语言、字体大小)会频繁变化,而用户身份信息(用户名、角色)很少变化,你会如何组织 Context?请说明理由。


思考题 4

假设 ThemeProvider 被嵌套在另一个 ThemeProvider 内部,内层的 value 会覆盖外层的 value。请举一个实际业务中需要这种嵌套 Provider 的场景。


思考题 5

在 React 19 的 Server Component 中,可以直接使用 useContext 吗?如果不能,正确的做法是什么?


实践作业

请在本案例基础上,扩展实现以下功能:

  1. ThemeContext 中增加 fontSize 字段(small | medium | large),并通过 ThemeProvider 提供修改方法
  2. 创建 FontSizeSelector 组件,可以在 NavBar 中切换字体大小
  3. 使用 useMemo 优化 ThemeProvidervalue
  4. useTheme 中增加类型安全校验,确保组件必须被 Provider 包裹

总结:Context 是 React 解决跨组件数据共享的核心工具。掌握它,需要记住三个关键点:

  1. Context 适用于共享且不频繁变化的数据(主题、语言、用户信息)
  2. Provider 的 value 要用 useMemo 缓存,避免无意义的重渲染
  3. 能用 props 解决的简单场景不要滥用 Context,保持组件的清晰和可复用性

在 React 19 中,Context 的核心 API 保持稳定,但与新的 Hooks 和 Server Components 的协同需要额外留意。希望本教程能帮助你建立起正确、系统的 Context 使用认知。

0
博主关闭了当前页面的评论