技术栈:React 19 + Vite
一、组件通信的四大场景
React 是单向数据流架构,数据只能从父组件流向子组件。在这个前提下,组件之间的通信分为四种关系:
1.1 父子组件通信
最基础的模式 ------ props 传递数据,自定义事件回传。
scss
父组件 ──props(data)──→ 子组件
父组件 ←──事件回调──── 子组件
父组件把数据通过 props 塞给子组件,子组件需要通知父组件时,调用父组件传下来的回调函数。这是 React 里最自然、最推荐的通信方式。而且因为数据源头在父组件手里,天然实现了共享数据、统一状态管理。
1.2 兄弟组件通信
两个平级组件没有直接的通道,必须通过它们共同的父组件中转:
css
父组件
├── 兄弟A ──回调→ 父组件(state变更) ──props→ 兄弟B
本质还是父子通信的组合 ------ 状态提升(lifting state up)。
1.3 爷孙 / 跨层级通信
这才是真正的痛点。爷组件要给重孙传数据:
scss
爷组件
└── 父组件 (不需要这个数据,但要帮着传)
└── 子组件 (不需要这个数据,但也要帮着传)
└── 孙组件 (终于用上了)
原来的那套层层传递,层次太深,搬运工作就太那啥了。
中间每一层都被迫接收并转发自己根本不需要的 props,只是当个"搬运工"。项目越复杂、组件树越深,这种 prop drilling 就越让人烦躁。
1.4 陌生人关系
两个组件在组件树上没有任何血缘关系,既不是父子也不是兄弟。比如侧边栏里的主题切换按钮,和主内容区的文章卡片,它们在不同分支上,但需要共享同一个主题状态。
二、useContext:跨层级直通车
2.1 面对"组件层次比较深"的场景
React 提供了一个原生方案 ------ Context(上下文)。思路很简单:
- 最外层 放一个
Context.Provider组件,把数据注入到组件树中 - 树中的任意后代组件,不管隔了多少层,都能直接**消费(读取)**这份数据
- 不需要考虑层级,连接上下文就能消费数据
一句话概括:
useContext 可以跨层级直接读取组件上下文数据,省去逐层向下传递 props。
2.2 三步走
| 步骤 | API | 做什么 | 对应文件 |
|---|---|---|---|
| ① | createContext |
创建上下文,定默认值 | ThemeContext.jsx |
| ② | Provider 组件 |
包裹组件树,注入数据 | App2.jsx |
| ③ | useContext |
后代组件消费数据 | Page.jsx、Child.jsx |
第一步:创建上下文
js
// src/ThemeContext.jsx
import { createContext } from 'react';
export const ThemeContext = createContext("light"); // 默认值
第二步:Provider 在最外层提供数据
jsx
// src/App2.jsx
<ThemeContext.Provider value={theme}>
<Page /> {/* Page 和它的所有后代都能拿到 theme */}
</ThemeContext.Provider>
Provider 不要求全局 ------ 你想包哪棵子树就包哪棵,灵活得很。value 一变,子树里所有消费了这个 Context 的组件自动重渲染。
第三步:后代任意层级直接消费
jsx
// src/components/Page.jsx ------ 第一层
const theme = useTheme(); // 拿到 "light" 或 "dark"
// src/components/Child.jsx ------ 第二层(Page 的子组件)
const theme = useTheme(); // 同样拿到,Page 没有通过 props 传给它
Page 没有给 Child 传 props ,但 Child 自己调用 useContext 直接拿到了主题值。这就是跨层级的威力。
三、自定义 Hooks:把响应式逻辑封装起来
3.1 什么是自定义 Hook?
React 全面进入 Hooks 编程时代后,不仅能用官方提供的 useState、useEffect、useContext,还能自己造轮子。
自定义 Hook 的约定:
- 函数名以
use开头 - 放在
hooks/目录下,属于项目架构层 - 比普通工具函数多一个能力:能把 响应式状态 (state)和副作用(effect)封装进去,随处复用
3.2 useTheme:封装 useContext
js
// src/hooks/useTheme.js
import { ThemeContext } from '../ThemeContext';
import { useContext } from 'react';
export function useTheme() {
return useContext(ThemeContext);
}
为什么要多此一举包一层?因为消费组件不需要知道背后是 Context 还是 Redux 还是 localStorage。它只认识 useTheme()。将来换数据源,只改 hooks 这一个文件,所有消费方无感。
3.3 useMouse:封装副作用
笔记里写了这个需求:
监听鼠标移动事件,鼠标坐标显示在页面上
------ 抽象出响应式的鼠标坐标,封装到 hooks 目录下
核心思路是把"监听 DOM 事件 → 更新 state"这套响应式流程,抽象成一个可复用的自定义 Hook:
js
// src/hooks/useMouse.js
import { useState, useEffect } from 'react';
export 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 };
};
使用时只需要一行:
jsx
// src/App.jsx
const { x, y } = useMouse();
// 组件只管渲染,Hook 负责"监听事件 → 更新 state → 触发重新渲染"
这就是自定义 Hook 的价值:把"监听鼠标移动"这件脏活累活锁在一个函数里,使用时跟调用普通函数一样简单,但它内部是完整响应式的。
四、通信方式全景对比
| 通信关系 | 用什么 | 一句话 |
|---|---|---|
| 父子 | props | 父传子读,回调回传 |
| 兄弟 | 状态提升到共同父组件 | 借道父组件中转 |
| 爷孙(层次较深) | Context + Provider + useContext | 跨层级直达,不搬运 |
| 陌生人 | Context 或全局状态库 | 共享数据源,各自消费 |
选择原则:能 props 就 props,层次太深才上 Context。不要一上来就全局 Context ------ 简单的父子关系用 props 可读性更好。
五、总结:两条主线
这个项目其实在讲两件事:
自定义 Hook = 有状态 + 有副作用的可复用函数
把"监听鼠标移动"这种响应式逻辑抽到 hooks 目录,组件只需调用一行。
Context = 跨层级的数据直通车createContext → Provider 提供 → useContext 消费,不考虑层级,跳过搬运工。
两者结合 ------ 用 Context 共享数据、用自定义 Hook 封装消费逻辑 ------ 就是 React 组件通信的最佳实践。
写于 2026-08-10