React Context + 自定义 Hooks:告别 Props 地狱,一行代码跨层级传数据
从"层层传递 props"的噩梦讲起,用主题切换和鼠标追踪两个实例,带你彻底搞懂 React Context 和自定义 Hooks。
前言
你有没有遇到过这种情况:一个数据在最外层组件获取,但真正用到它的却是第五层嵌套的子组件?你不得不在中间的每一层都加上 props 传递,哪怕那些中间组件根本不关心这个数据。
React Context 就是来解决这个问题的。这篇文章适合刚接触 React、已经会写 useState 和父子组件通信,但还没搞懂"跨层级传数据"怎么优雅实现的你。
读完你将收获:理解 Context 三件套(createContext / Provider / useContext)、学会封装自定义 Hooks、看懂一个完整项目的架构思路。
文末附有完整可运行项目代码,可以直接 clone 下来跑起来看效果。
项目概览
这个项目用两个 Demo 展示了 Context 和自定义 Hooks 的用法:
| Demo | 文件 | 演示内容 |
|---|---|---|
| 鼠标追踪 | App.jsx + useMouse.js |
自定义 Hook 封装鼠标坐标 |
| 主题切换 | App2.jsx + Theme.Context.jsx + useTheme.js |
Context 跨层级共享主题 |
组件树
css
App
├── ThemeContext.Provider(提供主题数据)
│ └── Page(消费主题,显示当前 theme 值)
│ └── Child(消费主题,按钮随主题变色)
└── button(点击切换 light / dark)
用到的核心技术
createContext--- 创建上下文容器.Provider--- 提供数据useContext--- 消费数据- 自定义 Hooks(
useTheme、useMouse) useState+useEffect
核心知识点一:Context --- 跨层级的数据管道
问题:Props 地狱长什么样?
假设没有 Context,你要把一个 theme 数据从 App 传给最深层的 Child:
jsx
// ❌ props 层层搬运,中间组件被迫传递自己不用的数据
function App() {
const theme = 'dark';
return <Page theme={theme} />;
}
function Page({ theme }) {
return <Child theme={theme} />; // Page 自己根本不用 theme
}
function Child({ theme }) {
return <button className={theme}>按钮</button>; // 只有它真正需要
}
项目 App2.jsx 里有一段被注释掉的代码,恰好就是这种"逐层传递"的写照:
jsx
// App2.jsx 注释代码
// <Parent>
// <Child>
// <GrandChild>
// <GreatGrandChild></GreatGrandChild>
// </GrandChild>
// </Child>
// </Parent>
如果嵌套到第四层、第五层,每层都要做 props "搬运工",代码又乱又容易出错。
解决:Context 三件套
React Context 就像一条直达管道------数据在最外层注入,任意深层组件都能直接读取,中间层无需参与。
第一步:createContext 创建管道
jsx
// Theme.Context.jsx
import { createContext } from 'react';
export const ThemeContext = createContext("light");
createContext("light") 做了两件事:
- 创建一个上下文对象(管道)
"light"作为默认值------当组件树没有被 Provider 包裹时,取这个值
第二步:Provider 注入数据
jsx
// App2.jsx
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Page />
<button onClick={() =>
setTheme(theme === 'light' ? 'dark' : 'light')
}>切换主题</button>
</ThemeContext.Provider>
);
}
Provider 是一个"数据发射器",value={theme} 告诉它当前要共享的数据是什么。
重点 :Provider 不需要放在全局------你想让哪个子树共享数据,就在哪里包裹。这里的 ThemeContext.Provider 只包裹了 <Page /> 和按钮,这就是它的"作用范围"。
第三步:useContext 消费数据
项目中 Page 和 Child 都直接读取了 theme,但没有经过任何中间层传递:
jsx
// Page.jsx
const Page = () => {
const theme = useTheme(); // 直接用,不需要从 App 传下来
return (
<>
Page {theme}
<br />
<Child /> {/* 传给 Child 时根本没写 theme prop */}
</>
);
};
// Child.jsx
function Child() {
const theme = useTheme(); // 直接取!
return <button className={theme}>按钮{theme}</button>;
}
Context 的数据流转图:
scss
ThemeContext.Provider value="dark"
│
├── Page ──useContext()──→ "dark" ✅ 跳过 App,直接读
│ │
│ └── Child ──useContext()──→ "dark" ✅ 跳过 Page,直接读
每一层都跳过了 props,直接"伸手"从 Context 里取。这就是跨层级的本质。
核心知识点二:自定义 Hooks --- 把"能力"封装成一行调用
是什么?
自定义 Hook 就是一个以 use 开头的函数,它可以调用 React 内置 Hooks(useState、useEffect 等),然后把响应式逻辑打包成一个可复用的"工具箱"。
项目中封装了两个自定义 Hook:
useTheme:让 Context 消费更优雅
jsx
// hooks/useTheme.js
import { ThemeContext } from '../Theme.Context.jsx';
import { useContext } from 'react';
export function useTheme() {
return useContext(ThemeContext);
}
为什么多此一举封装一层? 直接在每个组件里写 useContext(ThemeContext) 不就完了?
三个原因:
- 语义化 :
useTheme()比useContext(ThemeContext)读起来更直观 - 解耦 :假如将来 Context 从
ThemeContext换成Redux或Zustand,只需要改useTheme.js一个文件,所有组件无感知 - IDE 友好 :输入
useTheme()自动提示清晰,不会跟其他 Context 搞混
useMouse:封装副作用逻辑
这是项目另一个 Demo ------ 追踪鼠标坐标:
jsx
// hooks/useMouse.js
import { useState, useEffect } from 'react';
export const useMouse = () => {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useEffect(() => {
const handleMouseMove = (e) => {
setX(e.clientX);
setY(e.clientY);
};
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return { x, y };
};
逐行拆解:
| 代码 | 作用 |
|---|---|
const [x, setX] = useState(0) |
响应式存储鼠标 X 坐标,初始值 0 |
const [y, setY] = useState(0) |
Y 坐标同上 |
useEffect(..., []) |
组件挂载时执行一次,[] 表示不依赖任何变量 |
document.addEventListener('mousemove', handleMouseMove) |
监听全局鼠标移动事件 |
handleMouseMove |
每次鼠标移动,更新 x、y 状态 |
return () => removeEventListener(...) |
清理函数------组件卸载时移除监听,防止内存泄漏 |
return { x, y } |
对外暴露坐标,让调用方想怎么用就怎么用 |
使用方极其简洁:
jsx
// App.jsx
function App() {
const { x, y } = useMouse();
return (
<div>
{x && y ? `x:${x}, y:${y}` : '鼠标未移动'}
</div>
);
}
一行 useMouse() 就搞定了所有状态管理 + 事件监听 + 清理逻辑。这就是自定义 Hook 的威力------把复杂的响应式逻辑封装成一个"黑盒",使用时只需要一行调用。
知识串联:Context + 自定义 Hooks = 完美搭档
把两者结合起来,就形成了项目中这套架构模式:
scss
Theme.Context.jsx ← createContext 建管道
↓
hooks/useTheme.js ← 自定义 Hook 封装 useContext
↓
components/Page.jsx ← 调用 useTheme(),一行消费
components/Child.jsx ← 同上,完全不需要 props
这就是 React 推荐的"逻辑层 → 视图层"分层方式:
- Context 负责"数据管道"
- 自定义 Hooks 负责"消费数据的逻辑"
- 组件只负责"渲染 UI"
总结
回顾你从这个项目能掌握的:
- Context 是跨层级共享数据的管道 ------
createContext建管道,Provider注数据,useContext取数据 - 不需要把 Provider 包在全局------包在哪个子树,数据就在哪个范围共享
- 自定义 Hook 本质是逻辑封装------把 useState、useEffect 等打包成一行调用的"黑盒"
useTheme展示了"解耦"的价值------组件不需要知道 Context 的具体实现useMouse展示了"副作用管理"的规范写法------监听、更新、清理三步走,useEffect 里不要忘记 return 清理函数
下一步可以尝试的扩展
- 给
useMouse加一个节流(throttle),减少 setState 频率 - 用 Context 实现全局的用户登录状态(
UserContext,提供user和setUser) - 学习
useReducer+ Context 的组合,处理更复杂的状态逻辑
完整项目代码
项目托管在 Gitee,可以直接 clone 运行:
bash
# 1. 克隆仓库
git clone git@gitee.com:dcx2758/ai_doubao_dcx.git
cd ai_doubao_dcx/dcx/fe/react/basic/context-demo/context-demo/
# 2. 安装依赖
npm install
# 3. 启动开发服务器
npm run dev
项目文件树
bash
context-demo/
├── index.html
├── package.json
├── vite.config.js
├── eslint.config.js
├── src/
│ ├── main.jsx # 入口文件
│ ├── App.jsx # Demo1: useMouse 鼠标追踪
│ ├── App2.jsx # Demo2: Context 主题切换(手动改为 main.jsx 引入)
│ ├── Theme.Context.jsx # createContext 创建主题上下文
│ ├── index.css # 全局样式(CSS 变量 + 暗色模式)
│ ├── App.css # App 样式
│ ├── hooks/
│ │ ├── useMouse.js # 自定义 Hook: 鼠标坐标
│ │ └── useTheme.js # 自定义 Hook: 消费主题上下文
│ └── components/
│ ├── Page.jsx # 页面组件,消费主题
│ └── Child.jsx # 子组件,消费主题
依赖及版本
| 依赖 | 版本 |
|---|---|
| react | ^19.2.6 |
| react-dom | ^19.2.6 |
| vite | ^8.0.12 |
| @vitejs/plugin-react | ^6.0.1 |
提示 :默认
main.jsx引入的是App.jsx(鼠标追踪 Demo)。如果想体验主题切换 Demo,把import App from './App.jsx'改为import App from './App2.jsx',然后在切换主题的页面中观察 Page 和 Child 如何跨层级拿到数据。
你觉得 Context 这种方式怎么样?在你的项目里有没有遇到过"props 地狱"?欢迎在评论区交流 👏