写在前面:今天学了一个让我"拍大腿"的知识------useContext(上下文) 。以前父子通信,爷爷要把数据传给孙子,得一层一层往下递 props:爷爷给爸爸,爸爸给儿子,儿子给孙子......传个数据跟"击鼓传花"似的,中间层的组件明明不用这个数据,也得帮忙"搬运"。老师说:"层次太深,搬运工作就太那啥了。" 于是 React 提供了 Context------数据放在"云端",任何层级的组件都能直接"取用",不用层层递。 我们还用自定义 Hook 把鼠标坐标、主题色都封装了起来,代码瞬间清爽。
一、组件通信的"进化史"
1.1 传统的通信方式
老师说:
"父子组件通信------props 传递数据、自定义事件,共享数据,统一数据状态管理。兄弟关系、爷孙关系、陌生人关系......原来的那套层层传递,层次太深,搬运工作就太那啥了。"
传统的 props 逐层传递:
App(爷爷,有 theme 数据)
↓ props.theme
Parent(爸爸,只是搬运工!)
↓ props.theme
Child(儿子,还是搬运工!)
↓ props.theme
GrandChild(孙子,终于用上了)
问题:中间层(Parent、Child)根本不用 theme,但必须帮忙传递------又丑又烦。
老师说:
"组件层次比较深------
useContext上下文:提供数据上下文(最外层Context.Provider组件),消费上下文(跨越层级,不考虑层级)。"
1.2 useContext 的解决方案
arduino
App(爷爷,提供数据)
↓ ThemeContext.Provider 把数据放到"云端"
Parent(不需要传 props 了!)
Child(也不需要传 props 了!)
GrandChild(直接从"云端"拿数据)
数据从"逐层搬运"变成了"云端直取"。
二、useContext 三步走
2.1 第一步:createContext 创建上下文
看 ThemeContext.jsx:
jsx
import { createContext } from 'react';
// 创建一个 Theme 上下文,默认值是 'light'
export const ThemeContext = createContext('light');
createContext('light') 创建了一个上下文对象,默认值是 'light'。
这个上下文就是"云端"的入口------之后 Provider 往里放数据,组件从里取数据。
2.2 第二步:Provider 包裹组件树
看 App2.jsx:
jsx
import { useState } from 'react';
import { ThemeContext } from './ThemeContext';
import Page from './components/Page';
function App() {
const [theme, setTheme] = useState('light');
return (
{/* 上下文提供者:Provider 容器 */}
<ThemeContext.Provider value={theme}>
<Page />
<br />
<button onClick={() => setTheme('dark')}>切换主题</button>
</ThemeContext.Provider>
);
}
ThemeContext.Provider 是"数据提供者":
value={theme}:把当前的主题值放进上下文。- 所有被 Provider 包裹的组件(包括深层子组件)都能访问这个值。
- 状态一改(
setTheme('dark')),所有消费方自动更新。
老师说:
"并不是需要全局,任何地方作为容器使用。默认值
light,可以通过value来改变。"
2.3 第三步:useContext 消费数据
看 Page.jsx:
jsx
import Child from './Child';
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
const Page = () => {
const theme = useContext(ThemeContext); // 直接从"云端"取数据
console.log(theme);
return (
<>
Page {theme}
<br />
<Child />
</>
);
};
useContext(ThemeContext) 一步拿到主题值------不管 Page 在第几层,不需要任何 props。
三步总结:
| 步骤 | 代码 | 比喻 |
|---|---|---|
| ① createContext | createContext('light') |
建好"云端仓库" |
| ② Provider 包裹 | <ThemeContext.Provider value={theme}> |
把数据放进仓库 |
| ③ useContext 消费 | useContext(ThemeContext) |
从仓库取数据 |
三、完整链路:切换主题
arduino
App 里点"切换主题"
↓
setTheme('dark')
↓
Provider 的 value 变成 'dark'
↓
所有消费方自动重新渲染
↓
Page 显示 "Page dark"
Child 显示 "Child dark"
数据实时同步------不用一层层通知,Provider 变了,整个组件树里的消费者全部感知。
四、自定义 Hook:把 useContext 封装成 useTheme
4.1 为什么封装?
老师说:
"React 全面 Hooks 编程,可以使用 React、React-router-dom 等提供的 Hooks,还可以自定义 Hook------
use开头的函数,自己封装的,简单好用。比普通函数的封装,多的地方是可以将 React 响应式、副作用等业务封装进去。"
看 useTheme.js:
jsx
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
// 约定以 use 开头
export function useTheme() {
return useContext(ThemeContext);
}
useTheme() 把 useContext(ThemeContext) 包了一层 ------组件里调用 useTheme() 就能拿到主题,不需要关心 Context 的具体实现。
使用对比:
jsx
// 直接用 useContext
const theme = useContext(ThemeContext);
// 用自定义 Hook(更语义化)
const theme = useTheme();
useTheme() 比 useContext(ThemeContext) 可读性更好------一看就知道是"拿主题"的。
五、自定义 Hook 实战:useMouse 跟踪鼠标
5.1 useMouse 封装
看 useMouse.js:
jsx
import { useState, useEffect } from 'react';
const useMouse = () => {
const [x, setX] = useState(null);
const [y, setY] = useState(null);
useEffect(() => {
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
}, []);
return { x, y };
};
export default useMouse;
useMouse 把"鼠标跟踪"的完整逻辑封装成了一个 Hook:
| 部分 | 做了什么 |
|---|---|
| useState | 响应式鼠标坐标 x、y |
| useEffect | 挂载时监听 mousemove,卸载时移除监听 |
| 返回 | { x, y } 响应式坐标 |
重点:useEffect 的清理函数(cleanup) ------removeEventListener 保证组件卸载时移除监听,避免内存泄漏。
5.2 组件里使用
看 App.jsx:
jsx
import useMouse from './hooks/useMouse';
function App() {
const { x, y } = useMouse(); // 一行代码拿到鼠标坐标
return (
<div style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{x && y ? `x:${x}, y:${y}` : '鼠标未移动'}
</div>
);
}
组件只需要一行 const { x, y } = useMouse()------监听、更新、清理全部由 Hook 内部搞定。
这就是自定义 Hook 的价值:把"响应式 + 副作用"的复杂逻辑封装起来,任何组件一行代码就能复用。
六、总结:useContext + 自定义 Hook
| 概念 | 说明 |
|---|---|
| createContext | 创建上下文,可设默认值 |
| Provider | 数据提供者,value 放数据 |
| useContext | 跨层级消费数据,省去逐层传 props |
| 自定义 Hook | use 开头的函数,封装响应式/副作用 |
| useTheme | 封装 useContext,语义化取主题 |
| useMouse | 封装鼠标监听,返回响应式坐标 |
| cleanup 清理 | useEffect 返回函数,卸载时清理监听 |
useContext 解决"跨层级传数据"的问题(不用逐层搬 props),自定义 Hook 解决"逻辑复用"的问题(一行代码用复杂逻辑)。两者组合,React 的代码既简洁又高效。
写在最后
今天最大的收获,是终于不用"击鼓传花"式地传 props 了。爷爷的数据,孙子直接用 useContext 拿,中间层彻底解放。而且自定义 Hook 的封装能力太强了------鼠标坐标、主题色,都是一行代码搞定,复用性拉满。
下次面试官问你:"useContext 和自定义 Hook 怎么用?"
你可以淡定地说:
"useContext 用于跨层级组件通信,解决 props 逐层传递的问题。三步:① createContext(默认值) 创建上下文;② 用 Context.Provider 包裹组件树,通过 value 提供数据;③ 任意层级的组件用 useContext(Context) 直接消费数据------数据变化时所有消费者自动更新。自定义 Hook 是 use 开头的函数,可以封装响应式状态和副作用逻辑,比如 useMouse() 封装鼠标监听(useEffect 监听 + cleanup 清理,返回响应式坐标),useTheme() 封装 useContext(ThemeContext) 让取主题更语义化。两者结合,既解决了跨层传数据,又实现了逻辑复用。"
然后看着面试官满意的表情,心里默念:这波,又稳了。
本文所有代码示例均来自课堂学习资料,真实可运行。