React Context 与自定义 Hooks 学习分享
一、组件通信的痛点
在 React 应用中,组件树往往是多层次的。我们常见的组件关系包括:
- 父子组件通信:通过 props 实现单向数据流,父组件将数据通过 props 传递给子组件。
- 兄弟组件通信:需要借助共同的父组件来中转数据。
- 爷孙组件通信:传统方式是逐层传递 props,层次一深,"搬运工作"就变得繁琐且难以维护。
- 陌生人组件通信:两个毫无直接关系的组件共享数据,props 传递几乎不可行。
当组件层次比较深时,逐层传递 props 的方式会带来大量冗余代码,这就是 React Context(上下文)要解决的核心问题。
二、useContext ------ 跨层级的数据共享
2.1 什么是 Context?
Context 提供了一种在组件树中共享数据的方式,无需显式地通过组件树逐层传递 props。它的核心思想是:
- 提供数据上下文 :在组件树的最外层通过
Context.Provider提供数据。 - 消费上下文:任意层级的后代组件都可以跨越层级,直接连接上下文、消费数据。
2.2 使用 Context 的三个步骤
第一步:createContext 创建上下文
在 ThemeContext.jsx 中,我们使用 createContext 创建了一个主题上下文:
javascript
import { createContext } from 'react';
export const ThemeContext = createContext("light");
createContext("light") 创建了一个上下文对象,"light" 是默认值------当组件树中没有匹配的 Provider 时,会使用这个默认值。
第二步:Provider 组件包裹组件树
Context.Provider 组件用于包裹需要共享数据的那部分组件树,通过 value 属性向下传递数据。Provider 可以放在组件树的任意层级,其内部的所有后代组件都能访问到这个上下文。
第三步:useContext 消费上下文
在 useTheme.js 中,我们使用 useContext 来消费上下文:
javascript
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
export function useTheme() {
return useContext(ThemeContext);
}
useContext(ThemeContext) 接收一个 context 对象并返回该 context 的当前值。当前值由上层组件中距离最近的 <ThemeContext.Provider> 的 value 属性决定。
三、组件树中的实际消费
在 Page.jsx 和 Child.jsx 中,我们看到了跨层级消费的完整示例:
Page 组件:
javascript
import Child from './Child';
import { useTheme } from '../hooks/useTheme';
const Page = () => {
const theme = useTheme();
console.log(theme);
return (
<>
Page{theme}
<br />
<Child />
</>
);
};
Child 组件:
javascript
import { useTheme } from '../hooks/useTheme';
function Child() {
const theme = useTheme();
console.log(theme);
return (
<>
Child
<button className={theme}>按钮{theme}</button>
</>
);
}
这里 Page 和 Child 是嵌套关系(Page 包含 Child),但它们不需要通过 props 逐层传递 theme 数据。无论组件嵌套多深,只要在同一个 Context.Provider 下,任何层级的组件都可以直接通过 useContext 获取共享数据。 这就是 Context "跨越层级,不考虑层级"的含义。
四、自定义 Hooks ------ 响应式逻辑的封装与复用
4.1 什么是自定义 Hooks?
自定义 Hooks 是以 use 开头的函数,通常放在 hooks 目录下,属于架构层面的抽象。它相比普通函数封装,最大的区别是可以将 React 的响应式状态、副作用等业务逻辑封装进去,从而实现逻辑复用。
4.2 封装 useTheme
useTheme 就是一个典型的自定义 Hook,它将 useContext(ThemeContext) 的调用封装起来:
javascript
export function useTheme() {
return useContext(ThemeContext);
}
这样做的好处是:
- 模块化抽离 :消费上下文的逻辑从组件中抽离到 hooks 目录,组件只需调用
useTheme()即可。 - 多处复用 :在 Provider 内任何层级的组件中,都可以通过
useTheme()来消费数据。 - 语义化 :
useTheme()比直接写useContext(ThemeContext)更具可读性。
4.3 封装 useMouse ------ 响应式鼠标坐标
在 useMouse.js 中,我们将"监听鼠标移动、获取坐标"这个需求抽象为自定义 Hook:
javascript
import { useState, useEffect } from 'react';
export const useMouse = () => {
const [x, setX] = useState(null);
const [y, setY] = useState(null);
useEffect(() => {
document.addEventListener('mousemove', handleMouseMove);
return () => {
// 函数组件卸载后,不会主动回收的
// 定时器、worker、事件 需要手动回收
document.removeEventListener('mousemove', handleMouseMove);
};
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
}, []);
return { x, y };
};
这个 Hook 涉及了几个关键概念:
-
useState ------ 响应式状态 :
x和y是响应式的状态变量。当鼠标移动时,setX和setY更新状态,组件随之重新渲染,页面上显示的坐标实时更新。 -
useEffect ------ 副作用处理:
- 通过
document.addEventListener('mousemove', handleMouseMove)在document上注册鼠标移动事件。 - 返回一个清理函数 ,在组件卸载时执行
document.removeEventListener('mousemove', handleMouseMove)来移除事件监听。这是因为函数组件卸载后,定时器、Worker、事件监听等不会主动回收,必须手动清理,否则会造成内存泄漏。 - 依赖数组
[]表示这个 effect 只在组件挂载时执行一次,卸载时执行清理函数一次。
- 通过
-
封装与复用 :
useMouse将"监听鼠标移动并获取坐标"的完整逻辑封装在一个函数中。任何组件只需调用const { x, y } = useMouse()就能获得实时的鼠标坐标,无需关心内部实现细节。
4.4 在 App 中使用 useMouse
在 App.jsx 中,我们看到了 useMouse 的实际使用:
css
import { useState, useEffect } from 'react';
function App() {
const { x, y } = useMouse();
return (
<div style={{ height: '100vh', display: 'flex',
alignItems: 'center', justifyContent: 'center' }}>
{x && y ? `x: ${x}, y: ${y}` : '鼠标未移动'}
</div>
);
}
App 组件通过解构 useMouse() 的返回值获取 x 和 y,然后在页面中央显示鼠标坐标。当鼠标尚未移动时,x 和 y 为 null,显示"鼠标未移动";鼠标移动后实时显示坐标值。
五、总结
这篇文章围绕两个 React 核心概念展开,知识点归纳如下:
Context(上下文)
| 知识点 | 说明 |
|---|---|
| createContext | 用于创建一个上下文对象,可传入默认值 |
| Context.Provider | 上下文提供者组件,包裹组件树并通过 value 属性向下传递数据 |
| useContext | Hook,用于在任意层级的后代组件中消费上下文数据 |
| 跨层级通信 | Context 解决了爷孙组件、陌生人组件之间逐层传递 props 的痛点,实现"跨越层级,不考虑层级"的数据共享 |
自定义 Hooks
| 知识点 | 说明 |
|---|---|
| 命名约定 | 以 use 开头的函数,放在 hooks 目录下 |
| 封装能力 | 相比普通函数,自定义 Hook 可以封装响应式状态(useState)、副作用(useEffect)等 React 特性 |
| 逻辑复用 | 将可复用的业务逻辑抽离成 Hook,多个组件直接调用即可 |
| useState | 创建响应式状态变量,状态更新触发组件重新渲染 |
| useEffect | 处理副作用(如事件监听),返回的清理函数用于手动回收资源(定时器、事件监听、Worker 等),避免内存泄漏 |
依赖数组 [] |
空数组表示 effect 只在组件挂载和卸载时各执行一次 |
二者结合
- 自定义 Hook(如
useTheme)可以将useContext的调用封装起来,提供更简洁、语义化的 API。 - 无论是消费上下文还是监听鼠标事件,自定义 Hooks 都遵循"抽象 → 封装 → 复用"的思路,这正是 React Hooks 编程模式的核心价值。