层层传 props 传到吐?3 个步骤用 useContext 根治跨层传值,嵌套 10 层组件再也不写中间商代码

0 阅读7分钟

开篇:你经历过"祖孙传值"的噩梦吗?

一个很真实的场景:

你的组件嵌套长这样:

<App>
  <Parent>
    <Child>
      <GrandChild>
        <GrandGrandChild>  ← 真正要用数据的组件在这
        </GrandGrandChild>
      </GrandChild>
    </Child>
  </Parent>
</App>

屏幕截图 2026-08-08 000824.png 现在你想把主题颜色从 App 传到最深层的 GrandGrandChild

于是你开始写:

<Parent theme="dark">
  <Child theme="dark">
    <GrandChild theme="dark">
      <GrandGrandChild theme="dark" />  ← 终于到了
    </GrandChild>
  </Child>
</Parent>

每一层都要接收、再转发一遍。

中间两层根本不用这个数据,却被迫当"传话筒"。

数据一变,每一层 props 都要跟着改。代码越来越多,越来越乱。

这就是传说中的 props 层层透传(Prop Drilling)地狱。

今天这篇文章,就带你用 React 官方提供的 useContext3 个步骤彻底根治跨层传值

读完你会掌握:

  • 组件通信到底有哪几种关系?
  • createContext / Provider / useContext 三板斧怎么用
  • 自定义 Hook 怎么和 Context 组合,把代码优雅到极致
  • useMouse 这种自定义 Hook 的真实业务封装

一、先搞清楚:组件通信的 4 种关系

写 React 先要认清局势,组件间无非 4 种关系:

关系例子传统解法
父子App → Parentprops 传数据 + 自定义事件回调
兄弟A 组件 ↔ B 组件数据提升到共同父级
爷孙App → GrandGrandChildprops 层层传递(痛!)
陌生人毫无关系的组件全局状态管理(Redux / Zustand)

问题出在爷孙关系。

理论上你只用传一次,但因为 React 是单向数据流,数据只能一层一层往下走,中间层全成了"工具人"。

App (有数据)
 ↓ props
Parent (不用数据,只转发)
 ↓ props
Child (不用数据,只转发)
 ↓ props
GrandChild (不用数据,只转发)
 ↓ props
GrandGrandChild (终于用上了 😭)

单向数据流是 React 的规矩,但 useContext 能让你"跨层直达"。

二、useContext 一句话版解释

useContext = 让任意深度的子组件,跳过中间所有层级,直接读取最外层共享的数据。

核心思想就三个角色:

createContext   → 创建一个"共享数据仓库"
Context.Provider → 把数据放进仓库
useContext       → 任意层级的组件直接取数据

说白了:

你不需要每一层都递纸条了,把数据放进"公共储物柜",谁要用谁自己去拿。

三、第 1 步:createContext 创建上下文

新建 src/ThemeContext.jsx

import { createContext } from "react";

// 创建上下文,参数是默认值
export const ThemeContext = createContext("light");

要点:

  • createContext("light") 里的 "light"默认值
  • 如果组件没被 Provider 包裹,消费时就会拿到这个默认值
  • 实际项目中,默认值一般写"兜底值"——保证万一 Provider 忘了包,页面也不崩

四、第 2 步:Provider 提供数据

回到 App.jsx,用 Provider 把整个组件树包起来:

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

const App = () => {
  const [theme, setTheme] = useState("light");

  return (
    // 数据提供者:value 一变,下面所有消费方全部自动更新
    <ThemeContext.Provider value={theme}>
      <Page />
      <button onClick={() => setTheme("dark")}>切换主题:{theme}</button>
    </ThemeContext.Provider>
  );
};

export default App;

关键理解:

  • value={theme} 才是真正"给出去的数据"
  • Provider 是个容器,包在里面的所有组件都能拿到 value
  • 不需要全局使用,它可以在任何层级局部包裹,只给这一片组件提供数据

Provider 就像个"水龙头",它一开,下游所有管道都是甜的。

五、第 3 步:useContext 消费数据

最深层的组件,直接取:

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

const Child = () => {
  const theme = useContext(ThemeContext);

  return (
    <div>
      <h1>Child</h1>
      <button className={theme}>当前主题:{theme}</button>
    </div>
  );
};

export default Child;

看,中间没有任何 props!

  • ChildApp 之间隔了多少层组件都无所谓
  • useContext(ThemeContext) 一行代码,直接拿到最顶层的 theme
  • App 里的切换按钮,这个深层按钮的 className 自动同步变化

对比一下:

// ❌ 传 props:每一层都要写一遍
<Child theme="dark" onThemeChange={setTheme} />

// ✅ 用 Context:深层组件自己拿
const theme = useContext(ThemeContext);

省掉的不是几行代码,是"传话筒"组件的所有心智负担。

六、升级:自定义 Hook 封装,代码优雅到极致

直接用 useContext 已经很好用了,但每次都 import { useContext } + ThemeContext 太啰嗦。

把消费逻辑封装成自定义 Hook

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

// 约定:自定义 Hook 必须以 use 开头
export const useTheme = () => {
  return useContext(ThemeContext);
};

然后消费方简化为一行:

import { useTheme } from "./hooks/useTheme";

const Page = () => {
  const theme = useTheme();   // 一秒拿到主题
  return <div>Page:{theme}</div>;
};

为什么要把 Context 包进 Hook?

  1. 消费方更干净 —— 组件里不用知道 ThemeContext 长啥样
  2. 语义化 —— useTheme() 一看就知道是拿主题
  3. 统一入口 —— 将来想加默认值、加逻辑,只改 Hook 一处
  4. 符合 React 全面 Hooks 编程 —— 响应式状态、副作用、Context 全部收进 Hook

自定义 Hook 就是"把可复用的响应式逻辑装进 use 开头的函数里"。

七、第二个实战:useMouse 监听鼠标,顺便搞懂副作用清理

Context 讲完了,再看一个自定义 Hook 的经典案例——监听鼠标移动

需求:实时获取鼠标坐标并显示在页面上。

// src/hooks/useMouse.js
import { useState, useEffect } from "react";

export const useMouse = () => {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  const handleMouseMove = (e) => {
    setX(e.clientX);
    setY(e.clientY);
  };

  useEffect(() => {
    document.addEventListener("mousemove", handleMouseMove);

    // 清理函数:组件卸载时移除监听,防止内存泄漏
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
    };
  }, []);

  return { x, y };
};

组件里用起来极其简单:

import { useMouse } from "./hooks/useMouse";

const App = () => {
  const { x, y } = useMouse();

  return (
    <div style={{ height: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}>
      鼠标位置:{x} , {y}
    </div>
  );
};

export default App;

这段代码里有 2 个面试常考重点:

重点 1:useEffect 的清理函数

useEffect(() => {
  document.addEventListener("mousemove", handleMouseMove);

  return () => {           // ← 这就是清理函数
    document.removeEventListener("mousemove", handleMouseMove);
  };
}, []);

事件监听、定时器、网络请求这些"副作用",组件卸载后不会自动清理

不清理的后果:

  • 组件卸载了,监听器还挂在 document
  • 下一次又挂一个,越挂越多
  • 页面卡顿、内存泄漏

React 哲学:你在 useEffect 里"开了什么",就要在 return 里"关掉什么"。

重点 2:空依赖数组 []

}, []);
  • [] 表示"只在组件挂载时执行一次"
  • 监听器只注册一次,不会因为状态变化反复注册

八、一张图看懂整套架构

App.jsx
 └── ThemeContext.Provider value={theme}    ← 数据源头
      ├── Page.jsx
      │    └── Child.jsx  ← useContext / useTheme 直接拿数据
      │
      └── Button (切换主题,setTheme 更新 value)
                ↓
      value 一变 → 所有消费方自动重新渲染,中间层零改动

hooks/
 ├── useTheme.js   ← 封装 useContext(ThemeContext)
 └── useMouse.js   ← 封装鼠标监听副作用

文件结构就是你的架构:

src/
├── ThemeContext.jsx     ← createContext 创建仓库
├── hooks/
│   ├── useTheme.js      ← 消费 Context 的封装
│   └── useMouse.js      ← 复用副作用的封装
└── components/
    ├── Page.jsx
    └── Child.jsx

把"跨层数据"放进 Context,把"可复用逻辑"放进 Hooks —— 这就是 React 全面 Hooks 编程的正确打开方式。

九、踩坑提醒汇总

坑 1:useContext 必须在 Provider 内部使用

不在 Provider 里消费,拿到的是 createContext 的默认值。程序不报错,但数据不对——排查半天才发现 Provider 包错了层级。

坑 2:Provider 别包太大

有些人图省事,把整个应用都包进一个 Provider,所有状态全放一起。结果:一个状态变了,整棵组件树全重新渲染。

正确做法:数据按业务域拆分,主题归主题、用户归用户,谁需要谁包谁。

坑 3:自定义 Hook 必须 use 开头

useThemeuseMouse 这样的命名不是装样子。React 的规则引擎(ESLint 插件 react-hooks/rules-of-hooks)会按 use 前缀识别 Hook,不遵守规则会直接触发 lint 报错。

坑 4:useEffect 别忘了清理

特别是事件监听、定时器、轮询、WebSocket 连接,不清理必泄漏。养成"开了就关"的习惯。

十、总结

知识点一句话
createContext创建数据仓库,参数是默认值
Context.Provider提供数据,value 一变全员自动更新
useContext任意深度的组件直接消费数据
useTheme把消费逻辑封装成自定义 Hook
useMouse把副作用(事件监听)封装进 Hook
清理函数useEffect 开了什么,return 里就关什么

核心心法:

props 解决父子,Context 解决跨层,Hooks 解决复用。

三种手段配合使用,你的 React 组件树不管嵌套多深,数据传递都是"直达直取"。

完整源码已经整理在 context-demo 目录下,两个 Demo 都配好了:

  • App2.jsx:Context 跨层传主题的完整示例
  • App.jsx:useMouse 自定义 Hook 的完整示例

npm install && npm run dev 跑起来,亲手切一次主题、动一次鼠标,比看十遍文章都管用。