开篇:你经历过"祖孙传值"的噩梦吗?
一个很真实的场景:
你的组件嵌套长这样:
<App>
<Parent>
<Child>
<GrandChild>
<GrandGrandChild> ← 真正要用数据的组件在这
</GrandGrandChild>
</GrandChild>
</Child>
</Parent>
</App>
现在你想把主题颜色从
App 传到最深层的 GrandGrandChild。
于是你开始写:
<Parent theme="dark">
<Child theme="dark">
<GrandChild theme="dark">
<GrandGrandChild theme="dark" /> ← 终于到了
</GrandChild>
</Child>
</Parent>
每一层都要接收、再转发一遍。
中间两层根本不用这个数据,却被迫当"传话筒"。
数据一变,每一层 props 都要跟着改。代码越来越多,越来越乱。
这就是传说中的 props 层层透传(Prop Drilling)地狱。
今天这篇文章,就带你用 React 官方提供的 useContext,3 个步骤彻底根治跨层传值。
读完你会掌握:
- 组件通信到底有哪几种关系?
- createContext / Provider / useContext 三板斧怎么用
- 自定义 Hook 怎么和 Context 组合,把代码优雅到极致
- useMouse 这种自定义 Hook 的真实业务封装
一、先搞清楚:组件通信的 4 种关系
写 React 先要认清局势,组件间无非 4 种关系:
| 关系 | 例子 | 传统解法 |
|---|---|---|
| 父子 | App → Parent | props 传数据 + 自定义事件回调 |
| 兄弟 | A 组件 ↔ B 组件 | 数据提升到共同父级 |
| 爷孙 | App → GrandGrandChild | props 层层传递(痛!) |
| 陌生人 | 毫无关系的组件 | 全局状态管理(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!
Child和App之间隔了多少层组件都无所谓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?
- 消费方更干净 —— 组件里不用知道
ThemeContext长啥样 - 语义化 ——
useTheme()一看就知道是拿主题 - 统一入口 —— 将来想加默认值、加逻辑,只改 Hook 一处
- 符合 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 开头
useTheme、useMouse 这样的命名不是装样子。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 跑起来,亲手切一次主题、动一次鼠标,比看十遍文章都管用。