最近开发中,遇到了组件通讯的需求,我很难理清楚复杂项目中的通讯思路,本身经验不足,而且自己写的项目一般都是用状态管理工具resso或者zustand直接所有组件使用,或者直接父子之间传递,但是突然发现公司项目没有用到状态管理工具,而是用原生的useReducer对状态和操作状态方法进行归纳,用createContext创建上下文,然后用上下文组件为所有子组件提供共享的state和dispatch。
在 React 开发中,组件通讯是核心知识点之一。本文将通过一个完整案例,详细解析如何使用 createContext、useContext、useReducer、useRef 等 React 钩子实现不同场景下的组件通讯,并对每段代码进行逐行解释。
1、案例整体架构
我们实现了一个包含多页面的应用,主要功能包括:
1.计数器功能(增 / 减操作)
2.规则列表管理(添加 / 展示规则)
3.跨组件数据传递(通过 Context)
4.父子组件通讯(通过 Ref 调用子组件方法)
项目结构如下:
1.上下文管理:event-contexts.tsx(核心通讯逻辑)
2.页面组件:HomePage.tsx、DocsPage.tsx
3.布局组件:Layout.tsx(提供路由和上下文容器)
4.子组件:Event.tsx(通过 Ref 暴露方法的组件)
2、核心代码解析
1. 上下文定义与状态管理(event-contexts.tsx)
typescript
// 导入所需的 React 钩子
import { createContext, useContext, useReducer, useRef, useState } from "react";
import Event from "../pages/event";
// 定义初始状态
const initalState = {
number: 0, // 计数器数值
rules: ["孙悟空"], // 规则列表初始值
exportDate: "", // 从子组件获取的数据
exportUnit: "", // 数据单位
};
// 定义 action 类型(约束 dispatch 可执行的操作)
type actionType = "increment" | "decrement" | "addRule" | "export";
// 定义 action 接口(描述 action 的结构)
interface action {
type: actionType; // 操作类型
payload?: number; // 计数器增减的数值(可选)
rule?: string; // 新增的规则(可选)
exportDate?: string; // 要导出的日期(可选)
exportUnit?: string; // 要导出的单位(可选)
}
// 定义 reducer 函数(处理状态更新的纯函数)
const reducer = (state: typeof initalState, action: action) => {
switch (action.type) {
case "increment":
// 计数器增加:基于当前状态创建新对象(不可变更新)
return { ...state, number: state.number + action.payload };
case "decrement":
// 计数器减少
return { ...state, number: state.number - action.payload };
case "addRule":
// 添加规则:复制原数组并添加新元素
return { ...state, rules: [...state.rules, action.rule || ""] };
case "export":
// 导出数据:更新从子组件获取的值
return {
...state,
exportDate: action.exportDate || "",
exportUnit: action.exportUnit || "",
};
default:
return state;
}
};
// 创建上下文对象(定义上下文的类型和默认值)
const CountContext = createContext<{
state: typeof initalState;
dispatch: React.Dispatch<action>;
}>({
state: initalState,
dispatch: () => {}, // 默认的空 dispatch
});
// 定义 Provider 组件(提供上下文数据)
export const CountProvider = ({ children }) => {
// 创建子组件的 Ref(用于调用子组件方法)
const eventRef = useRef(null);
// 使用 useReducer 管理状态(替代 useState 处理复杂状态逻辑)
const [state, dispatch] = useReducer(reducer, initalState);
// 用于存储从子组件获取的值(局部状态)
const [childValue, setChildValue] = useState("");
// 处理点击事件:获取子组件数据并更新上下文
const handleClick = () => {
// 通过 Ref 调用子组件暴露的 getValue 方法
const value = eventRef?.current?.getValue() || "";
setChildValue(value);
// 分发 action 更新上下文状态
dispatch({ type: "export", exportDate: value, exportUnit: "元" });
};
return (
<>
{/* 渲染子组件并绑定 Ref */}
<Event ref={eventRef} />
{/* 触发获取子组件数据的按钮 */}
<button onClick={() => handleClick()}>
点我获取并且显示子组件暴露的数据
</button>
{/* 展示从子组件获取的数据 */}
<p>子组件暴露的数据:{state.exportDate}</p>
{/* 提供上下文数据给子组件树 */}
<CountContext.Provider value={{ state, dispatch }}>
{children} {/* 渲染子组件(路由页面) */}
</CountContext.Provider>
</>
);
};
// 自定义 Hook:简化上下文的使用
export const useCount = () => {
const context = useContext(CountContext);
// 校验:确保在 Provider 内部使用
if (!context) throw new Error("useCount 必须在 CountProvider 中使用");
return context;
};
核心钩子解析:
createContext:创建上下文容器,定义共享数据的结构useContext:在组件中获取上下文数据,避免 props 层层传递useReducer:处理复杂状态逻辑,通过 dispatch 分发 action 更新状态useRef:创建持久化的引用,用于访问 DOM 元素或子组件实例useState:管理组件内部的简单状态
2. 子组件(Event.tsx)- 通过 Ref 暴露方法
javascript
import React, {
useImperativeHandle, // 自定义暴露给父组件的方法
forwardRef, // 转发 Ref 给子组件
useState,
useRef,
} from "react";
// 使用 forwardRef 转发 Ref 到组件内部
const Event = forwardRef((props, ref) => {
// 子组件内部状态
const [childValue, setChildValue] = useState("猴子");
// 输入框的 Ref
const ref1 = useRef<HTMLInputElement>(null);
// 自定义暴露给父组件的方法(通过 Ref 访问)
useImperativeHandle(ref, () => ({
getValue: () => childValue, // 获取当前值
resetValue: () => resetValue(), // 重置值
}));
// 内部方法:获取值
const getValue = () => {
return childValue;
};
// 内部方法:重置值
const resetValue = () => {
setChildValue("");
};
// 提交输入框的值到组件状态
const submitValue = () => {
setChildValue(ref1?.current?.value || "");
};
return (
<div>
{/* 输入框:绑定 ref1 和 childValue */}
<input type="text" value={childValue} ref={ref1} />
{/* 提交按钮 */}
<button onClick={() => submitValue()}>提交</button>
</div>
);
});
export default Event;
核心钩子解析:
forwardRef:允许父组件将 Ref 传递到子组件内部,突破组件边界useImperativeHandle:自定义子组件通过 Ref 暴露给父组件的方法,避免暴露整个组件实例useRef(DOM 引用):访问输入框 DOM 元素,获取用户输入值
3.布局组件(Layout.tsx)- 提供上下文容器
javascript
import { Link, Outlet } from "umi"; // Umi 框架的路由组件
import styles from "./index.less";
import { CountProvider } from "../contexts/event-contexts";
export default function Layout() {
return (
<div className={styles.navs}>
{/* 导航菜单 */}
<ul>
<li>
<Link to="/">Home</Link> {/* 首页路由 */}
</li>
<li>
<Link to="/docs">Docs</Link> {/* 文档页路由 */}
</li>
<li>
<a href="https://github.com/umijs/umi">Github</a>
</li>
</ul>
{/* 上下文提供者:包裹路由出口,使所有页面都能访问上下文 */}
<CountProvider>
<Outlet /> {/* 路由出口:渲染匹配的页面组件 */}
</CountProvider>
</div>
);
}
功能说明:
- 通过
CountProvider包裹<Outlet />,确保所有路由页面都能访问上下文 - 使用 Umi 的
Link和Outlet实现路由跳转和页面渲染
4. 首页组件(HomePage.tsx)- 使用上下文数据
javascript
import yayJpg from "../assets/yay.jpg";
import { useCount } from "../contexts/event-contexts"; // 导入自定义 Hook
export default function HomePage() {
// 通过自定义 Hook 获取上下文数据
const { state, dispatch } = useCount();
return (
<div>
{/* 遍历展示规则列表 */}
{state.rules.map((item) => (
<p key={item}>{item}</p>
))}
{/* 展示从子组件获取的数据和单位 */}
「{state.exportDate}
{state.exportUnit}」
</div>
);
}
功能说明:
- 使用
useCount获取上下文的state和dispatch - 展示
state.rules列表和state.exportDate数据
5. 文档页组件(DocsPage.tsx)- 操作上下文状态
javascript
import { useCount } from "../contexts/event-contexts";
import { useState } from "react";
const DocsPage = () => {
// 获取上下文数据
const { state, dispatch } = useCount();
// 本地状态:管理输入框的值
const [name, setName] = useState("");
return (
<div>
{/* 展示计数器数值 */}
<p>number: {state.number}</p>
{/* 计数器增加按钮:分发 increment action */}
<button onClick={() => dispatch({ type: "increment", payload: 1 })}>
increment
</button>
{/* 计数器减少按钮:分发 decrement action */}
<button onClick={() => dispatch({ type: "decrement", payload: 1 })}>
decrement
</button>
{/* 展示规则列表 */}
{state.rules.map((item) => (
<p key={item}>{item}</p>
))}
{/* 输入框:用于添加新规则 */}
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)} // 更新本地状态
/>
{/* 添加规则按钮:分发 addRule action */}
<button onClick={() => dispatch({ type: "addRule", rule: name })}>
addRule
</button>
</div>
);
};
export default DocsPage;
功能说明:
- 使用
useState管理输入框的本地状态 - 通过
dispatch分发不同类型的 action 操作上下文状态 - 实现计数器增减和添加规则的功能
3、总结:React 组件通讯方式对比
-
Context + useReducer
- 适用场景:跨组件(多层级)数据共享
- 优势:避免 props 透传,集中管理状态逻辑
- 核心 API:
createContext、useContext、useReducer
-
Ref + useImperativeHandle
- 适用场景:父组件需要直接操作子组件(调用方法 / 获取数据)
- 优势:直接访问子组件实例,适合控制型交互
- 核心 API:
useRef、forwardRef、useImperativeHandle
通过本案例,我们完整实现了从简单到复杂的组件通讯场景,掌握这些钩子的使用方式可以有效解决 React 应用中的数据传递问题。实际开发中,应根据具体场景选择合适的通讯方式,以提高代码的可维护性和扩展性
图例
编辑