文章目录
-
- 一、前言:什么是"上下文"?
- 二、组件通信的四种关系
- 三、当组件层次很深时——useContext 登场
-
- 3.1 它解决了什么问题?
- 3.2 核心角色:Provider 与 Consumer
- 四、useContext 三步走(实战完整代码)
-
- 第一步:createContext 创建上下文
- 第二步:Provider 包裹组件树
- 第三步:useContext 消费上下文
- 五、自定义 Hooks:把上下文消费封装起来
-
- 5.1 为什么需要自定义 Hook?
- 5.2 封装 useTheme Hook
- 5.3 自定义 Hook 的本质
- 六、综合实战:监听鼠标移动,坐标实时显示
-
- 6.1 需求分析
- 6.2 完整代码实现
- 6.3 在组件中使用
- 6.4 代码执行流程解析
- 6.5 和 Context 的关系
- 七、全文总结
-
- 7.1 知识路径回顾
- 7.2 核心知识点复盘
- 7.3 常见问题 / 避坑指南
一、前言:什么是"上下文"?
在 React 开发中,“上下文”(Context)这个词并不陌生。你可以把它想象成一颗组件树的"全局广播站"——某个组件在上面"喊一声"(提供数据),树中任意深度的子组件都能"听到"(消费数据),而不需要中间每一层组件帮忙传话。
但在理解 Context 之前,我们有必要先回顾一下 React 中组件通信的各种场景,这样才能真正明白 为什么需要 useContext。
二、组件通信的四种关系
React 应用由组件构成,组件之间必然要传递数据。根据组件之间的层级关系,通信方式可以归纳为以下四种。
1 父子组件通信
这是 React 中最基础、最常见的通信方式,核心是单向数据流:
数据从父组件流向子组件,通过 props 传递。
子组件不能直接修改父组件的数据,而是通过父组件传递的回调函数来通知父组件。
代码示例:父传子 + 子通知父
// 父组件
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<h2>父组件 —— 当前计数:{count}</h2>
{/* ① 通过 props 把数据传给子组件 */}
{/* ② 同时把修改数据的方法也传给子组件 */}
<Child count={count} onIncrement={() => setCount(count + 1)} />
</div>
);
}
// 子组件
function Child({ count, onIncrement }) {
return (
<div>
<p>子组件接收到:{count}</p>
{/* 点击时调用父组件传下来的回调 */}
<button onClick={onIncrement}>+1</button>
</div>
);
}
解析:父组件通过 props 向下传递 count 数据,同时把 onIncrement 回调函数一并传下去,子组件点击按钮时调用这个回调,父组件更新 state,React 自动重新渲染。这就是典型的单向数据流 + 状态提升模式。
这种模式的优点在于:数据流向清晰、易于追踪调试,适合绝大多数简单场景。
兄弟组件通信
两个组件有同一个父组件,它们之间需要共享数据时,做法是把共享状态提升到它们共同的父组件中
兄弟组件之间不直接通信,而是通过"最近共同祖先"作为中转站。这和父子通信本质上是一种模式。
爷孙 / 跨层级组件通信
这是问题开始暴露的地方。假设组件层级如下:
爷爷(GrandParent)
└── 爸爸(Parent)
└── 儿子(Child)
└── 孙子(GrandChild)
如果爷爷组件的数据要传给孙子,用 props 就得这样写:
function GrandParent() {
const [theme, setTheme] = useState('dark');
return <Parent theme={theme} />;
}
function Parent({ theme }) {
// 这层根本不关心 theme,纯粹当"搬运工"
return <Child theme={theme} />;
}
function Child({ theme }) {
// 继续搬运…
return <GrandChild theme={theme} />;
}
function GrandChild({ theme }) {
// 终于用到 theme 了!
return <div>当前主题:{theme}</div>;
}
这就是经典的"Props 透传"(Prop Drilling)问题:中间组件被迫接收并转发自己根本不需要的 props,层次越深,搬运工作就越繁琐、越容易出错。一个中间组件改动了 props 名称,整个链条都要跟着改。
陌生人组件通信
两个组件在组件树中毫无关系(不同的页面、不同的路由分支),它们之间的数据共享就需要借助全局状态管理方案(如 Redux、Zustand)或 React Context 来实现。对于"不引入第三方库,又想全局共享数据"的场景,Context 就是 React 提供的原生解决方案。
三、当组件层次很深时——useContext 登场
3.1 它解决了什么问题?
上面提到的 Props 透传问题,随着项目变大,组件层级越来越深,“搬运 props” 会变成一种折磨:
- 中间组件被迫接收无关 props,代码变得臃肿
- 需求变更时(比如要加一个新 prop),每一层中间组件都得改
- 组件复用性变差——中间组件和特定 props 强耦合
useContext 的思路很简单:开一个"跨层级的数据通道",让数据提供者和数据消费者直接对话,中间组件完全不需要参与。
3.2 核心角色:Provider 与 Consumer
Context 机制中有两个关键角色:
| Provider(提供者) | 包裹在组件树最外层,负责"广播"数据 | 广播电台 |
| Consumer(消费者) | 组件树中任意深度的组件,负责"接收"数据 | 收音机 |
Provider 只管发,Consumer 只管收,中间隔了多少层完全不重要。
四、useContext 三步走(实战完整代码)
下面通过一个真实的主题切换案例,演示 useContext 的完整用法。代码来自项目实战,可直接运行。
第一步:createContext 创建上下文
// ThemeContext.jsx
// 创建一个主题上下文,为深层次的组件树提供主题共享数据
import { createContext } from 'react';
// createContext('light') 创建一个上下文对象
// 参数 'light' 是默认值 —— 当组件树中没有 Provider 时,就使用这个值
export const ThemeContext = createContext("light");
关键解析:
- createContext(defaultValue) 接收一个默认值作为参数。这个默认值只有在组件树中找不到对应 Provider 时才会生效。
- 返回的是一个 Context 对象,它身上自带 Provider 和 Consumer 两个属性。
- 通常将 Context 对象单独放在一个文件中,方便多处引入。
第二步:Provider 包裹组件树
// App.jsx
import { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Page from './components/Page';
function App() {
const [theme, setTheme] = useState('light');
return (
// ThemeContext.Provider 是上下文的"提供者"容器
// 注意:Provider 不是只能放在全局!任何组件都可以作为局部容器使用
// value 属性用于向子树"广播"数据,覆盖 createContext 的默认值
<ThemeContext.Provider value={theme}>
{/* Page 及其所有子孙组件,都能直接读取到 theme */}
<Page />
{/* 点击按钮切换主题:在 'light' 和 'dark' 之间切换 */}
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</ThemeContext.Provider>
);
}
export default App;
关键解析:
- <ThemeContext.Provider value={…}> 是提供数据的方式。value 是你想共享的任何数据——可以是字符串、数字、对象、甚至是另一个 state 的函数。
- Provider 不一定要放在应用最外层,它可以放在任意位置,只有它包裹的那部分组件树才能消费这个上下文。这是一种"局部全局"的设计。
- 当 value 的值变化时,所有消费该上下文的组件都会自动重新渲染,不需要手动通知。
第三步:useContext 消费上下文
任何被 Provider 包裹的组件(无论嵌套多深),都可以直接使用 useContext 读取上下文数据:
// components/Page.jsx
import Child from './Child';
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
const Page = () => {
// 一行代码,直接拿到主题数据 —— 不需要从父组件接收 props!
const theme = useContext(ThemeContext);
console.log(theme); // 'light' 或 'dark'
return (
<>
<p>Page 组件 —— 当前主题:{theme}</p>
<br />
{/* Child 组件里也消费同一份上下文,同样不需要传 props */}
<Child />
</>
);
};
export default Page;
再看 Child 组件,它甚至更深一层,同样可以消费上下文:
// components/Child.jsx
import { useTheme } from '@/hooks/useTheme';
function Child() {
// 使用自定义 hook 消费上下文(推荐方式,下文详述)
const theme = useTheme();
console.log(theme);
return (
<>
<p>Child 组件</p>
<button className={theme}>按钮 —— {theme}</button>
</>
);
}
export default Child;
效果总结:App 中点击按钮切换 theme state → Provider 的 value 变化 → Page 和 Child 自动用新 theme 重新渲染。整个过程,App 没有向 Page 传任何 props,Page 也没有向 Child 传 props。跨越层级,直接通信。
五、自定义 Hooks:把上下文消费封装起来
5.1 为什么需要自定义 Hook?
上面的示例中,Page.jsx 这样消费上下文:
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
const theme = useContext(ThemeContext);
这样写完全可以工作,但存在两个问题:
更好的做法是:把 useContext 调用封装成自定义 Hook,消费者只需要调用这个 Hook 即可。
5.2 封装 useTheme Hook
// hooks/useTheme.js
// React 全面进入 Hooks 编程时代,可以使用 React、react-router-dom 等提供的 Hooks
// 还可以自定义 Hook —— 以 use 开头的函数,自己封装的,简单好用
// 比普通函数封装更强的地方:可以把 React 的响应式、副作用等逻辑一并封装进去
// 在 Provider 包裹的任何层级组件中,多处消费数据,模块化抽离放到 hooks 目录下
import { ThemeContext } from '@/ThemeContext';
import { useContext } from 'react';
// 约定:自定义 Hook 必须以 use 开头
export function useTheme() {
// 内部封装了 useContext 调用,对外暴露的是一个干净的接口
return useContext(ThemeContext);
}
消费方使用就变得非常简洁:
// 只需要引入 useTheme 即可,不需要知道 ThemeContext 的存在
import { useTheme } from '@/hooks/useTheme';
function Child() {
const theme = useTheme(); // 干净利落
// …
}
自定义 Hook 的优势:
- 封装细节:消费者不需要知道底层是 Context 还是其他方式,换了实现也不影响使用方
- 统一管理:所有对 ThemeContext 的访问收敛到一个函数中,方便维护
- 可测试性:可以单独为 Hook 编写测试
- 模块化:放在 hooks/ 目录下,属于基础设施层 / 架构层代码
5.3 自定义 Hook 的本质
自定义 Hook 本质上就是以 use 开头的普通 JavaScript 函数。和普通函数的区别在于:
| 能否调用 React Hooks(useState、useEffect 等) | ✗ 不能 | ✓ 可以 |
| 能否封装响应式数据逻辑 | ✗ 不能 | ✓ 可以 |
| 命名规范 | 无强制要求 | 必须以 use 开头 |
| 适用场景 | 工具函数、纯逻辑 | 状态、副作用、Context 等 React 特性 |
六、综合实战:监听鼠标移动,坐标实时显示
掌握了 Context 和自定义 Hook,我们来看一个不涉及 Context、但同样体现自定义 Hook 价值的实战案例——封装鼠标坐标追踪。
6.1 需求分析
目标:在页面上实时显示鼠标的 X、Y 坐标。
思路抽象:把"追踪鼠标坐标"这段逻辑抽象成一个响应式的数据源——useMouse Hook。哪个组件需要鼠标坐标,直接调用它即可,不需要在每个组件里写一遍事件监听的代码。
6.2 完整代码实现
// hooks/useMouse.js
import { useState, useEffect } from 'react';
export const useMouse = () => {
// ① 用两个 state 分别存储鼠标的 x、y 坐标
const [x, setX] = useState(null);
const [y, setY] = useState(null);
// ② useEffect 监听副作用:绑定/解绑事件
useEffect(() => {
// ③ 事件处理函数:鼠标移动时更新 state
function handleMouseMove(e) {
setX(e.clientX); // clientX 是鼠标相对于浏览器窗口的水平坐标
setY(e.clientY); // clientY 是鼠标相对于浏览器窗口的垂直坐标
}
// ④ 绑定事件监听器
document.addEventListener('mousemove', handleMouseMove);
// ⑤ 清理函数:组件卸载时自动移除事件监听
// 定时器、Web Worker、事件监听等资源,React 不会主动回收
// 必须手动清理,否则会造成内存泄漏
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []); // 空依赖数组,只在组件挂载时执行一次
// ⑥ 返回坐标数据,供组件使用
return { x, y };
};
6.3 在组件中使用
// App.jsx
import { useMouse } from './hooks/useMouse';
function App() {
// 使用自定义 Hook 获取鼠标坐标 —— 代码非常简洁!
const { x, y } = useMouse();
return (
<div style={{
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
{/* x 和 y 为 null 表示鼠标还没移动过 */}
{x && y ? `当前鼠标坐标:x: ${x}, y: ${y}` : '鼠标未移动'}
</div>
);
}
export default App;
6.4 代码执行流程解析
组件首次渲染 → useMouse() 执行
→ useState(null) 创建 x、y 状态
→ useEffect 注册 mousemove 监听器
→ 返回 {x: null, y: null}
→ 组件渲染 "鼠标未移动"
用户移动鼠标 → handleMouseMove 被调用
→ setX / setY 更新状态
→ React 检测到状态变化
→ 组件重新渲染,显示最新坐标
组件卸载 → useEffect 的清理函数执行
→ removeEventListener 移除监听器
→ 避免内存泄漏
6.5 和 Context 的关系
注意:useMouse 这个 Hook 没有用到 Context,它展示的是自定义 Hook 的另一个重要用途——封装副作用(useEffect)和响应式数据(useState)。
#mermaid-svg-5BbMwOuOGK4zLdVO{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5BbMwOuOGK4zLdVO .error-icon{fill:#552222;}#mermaid-svg-5BbMwOuOGK4zLdVO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5BbMwOuOGK4zLdVO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5BbMwOuOGK4zLdVO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5BbMwOuOGK4zLdVO .marker.cross{stroke:#333333;}#mermaid-svg-5BbMwOuOGK4zLdVO svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5BbMwOuOGK4zLdVO p{margin:0;}#mermaid-svg-5BbMwOuOGK4zLdVO .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-5BbMwOuOGK4zLdVO .cluster-label text{fill:#333;}#mermaid-svg-5BbMwOuOGK4zLdVO .cluster-label span{color:#333;}#mermaid-svg-5BbMwOuOGK4zLdVO .cluster-label span p{background-color:transparent;}#mermaid-svg-5BbMwOuOGK4zLdVO .label text,#mermaid-svg-5BbMwOuOGK4zLdVO span{fill:#333;color:#333;}#mermaid-svg-5BbMwOuOGK4zLdVO .node rect,#mermaid-svg-5BbMwOuOGK4zLdVO .node circle,#mermaid-svg-5BbMwOuOGK4zLdVO .node ellipse,#mermaid-svg-5BbMwOuOGK4zLdVO .node polygon,#mermaid-svg-5BbMwOuOGK4zLdVO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5BbMwOuOGK4zLdVO .rough-node .label text,#mermaid-svg-5BbMwOuOGK4zLdVO .node .label text,#mermaid-svg-5BbMwOuOGK4zLdVO .image-shape .label,#mermaid-svg-5BbMwOuOGK4zLdVO .icon-shape .label{text-anchor:middle;}#mermaid-svg-5BbMwOuOGK4zLdVO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5BbMwOuOGK4zLdVO .rough-node .label,#mermaid-svg-5BbMwOuOGK4zLdVO .node .label,#mermaid-svg-5BbMwOuOGK4zLdVO .image-shape .label,#mermaid-svg-5BbMwOuOGK4zLdVO .icon-shape .label{text-align:center;}#mermaid-svg-5BbMwOuOGK4zLdVO .node.clickable{cursor:pointer;}#mermaid-svg-5BbMwOuOGK4zLdVO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5BbMwOuOGK4zLdVO .arrowheadPath{fill:#333333;}#mermaid-svg-5BbMwOuOGK4zLdVO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5BbMwOuOGK4zLdVO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5BbMwOuOGK4zLdVO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5BbMwOuOGK4zLdVO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5BbMwOuOGK4zLdVO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5BbMwOuOGK4zLdVO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5BbMwOuOGK4zLdVO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5BbMwOuOGK4zLdVO .cluster text{fill:#333;}#mermaid-svg-5BbMwOuOGK4zLdVO .cluster span{color:#333;}#mermaid-svg-5BbMwOuOGK4zLdVO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5BbMwOuOGK4zLdVO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5BbMwOuOGK4zLdVO rect.text{fill:none;stroke-width:0;}#mermaid-svg-5BbMwOuOGK4zLdVO .icon-shape,#mermaid-svg-5BbMwOuOGK4zLdVO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5BbMwOuOGK4zLdVO .icon-shape p,#mermaid-svg-5BbMwOuOGK4zLdVO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5BbMwOuOGK4zLdVO .icon-shape .label rect,#mermaid-svg-5BbMwOuOGK4zLdVO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5BbMwOuOGK4zLdVO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5BbMwOuOGK4zLdVO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5BbMwOuOGK4zLdVO :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
副作用场景
useMouse
封装 useState + useEffect复用状态逻辑
Context 场景
useTheme
封装 useContext跨组件共享数据
两者都是自定义 Hook,只是封装的 React 特性不同:
- Context 型 Hook(useTheme):解决"跨层级数据共享"
- 副作用型 Hook(useMouse):解决"响应式逻辑复用"
七、全文总结
7.1 知识路径回顾
本文从 React 组件通信的实际痛点出发,沿着以下路径逐步深入:
组件通信四种关系 → Props 透传问题 → Context 机制原理
→ createContext / Provider / useContext 三步法
→ 自定义 Hook 封装(useTheme)→ 副作用封装(useMouse)
核心结论:useContext 不是用来替代 props 的,而是在 props 传递变得繁琐时的一种优雅补充。对于浅层通信,props 足够且更直观;对于跨层级共享数据,Context 是最合适的原生方案。
7.2 核心知识点复盘
| 单向数据流 | React 数据从父到子通过 props 流动,是 React 的核心设计原则 |
| Props 透传 | 中间组件被迫转发不关心的 props,层次深时难以维护 |
| createContext | 创建一个上下文容器,参数为默认值 |
| Provider | 包裹组件树,通过 value 属性向下广播数据 |
| useContext | 在任意深度的子组件中直接读取上下文数据 |
| 自定义 Hook | 以 use 开头的函数,可封装 React Hooks 进行逻辑复用 |
| useEffect 清理 | 事件监听、定时器等资源必须在清理函数中手动回收 |
7.3 常见问题 / 避坑指南
Q1:什么时候用 Context,什么时候用状态管理库(Redux / Zustand)?
| 少量全局数据(主题、语言、用户信息) | Context 足够 |
| 频繁更新的状态(如动画帧、输入框实时值) | 不适合 Context,频繁变化会导致整个子树重渲染 |
| 复杂的状态逻辑、中间件、调试工具 | 状态管理库(Redux、Zustand) |
| 不想引入第三方依赖 | Context + useReducer 组合 |
Q2:Provider 的 value 变化时,所有消费组件都会重新渲染吗?
会。Provider 的 value 一旦变化,所有调用 useContext 的组件都会重新渲染,这是 Context 的性能代价。如果你的数据中有一部分变化频繁、另一部分相对稳定,建议拆分成多个 Context,让不相关的消费者免受影响。
Q3:自定义 Hook 为什么必须以 use 开头?
这是 React 的约定(也是 ESLint 规则的强制要求)。React 通过函数名是否以 use 开头来判断这个函数是否是一个 Hook,从而对 Hook 的调用规则(不能放在条件语句、循环中)进行静态检查。
Q4:useEffect 的清理函数什么时候执行?
两种情况下执行:
网硕互联帮助中心





评论前必须登录!
注册