React 组件通讯全案例解析:从 Context 到 Ref 的实战应用

最近开发中,遇到了组件通讯的需求,我很难理清楚复杂项目中的通讯思路,本身经验不足,而且自己写的项目一般都是用状态管理工具resso或者zustand直接所有组件使用,或者直接父子之间传递,但是突然发现公司项目没有用到状态管理工具,而是用原生的useReducer对状态和操作状态方法进行归纳,用createContext创建上下文,然后用上下文组件为所有子组件提供共享的state和dispatch。

在 React 开发中,组件通讯是核心知识点之一。本文将通过一个完整案例,详细解析如何使用 createContextuseContextuseReduceruseRef 等 React 钩子实现不同场景下的组件通讯,并对每段代码进行逐行解释。

1、案例整体架构

我们实现了一个包含多页面的应用,主要功能包括:

1.计数器功能(增 / 减操作)

2.规则列表管理(添加 / 展示规则)

3.跨组件数据传递(通过 Context)

4.父子组件通讯(通过 Ref 调用子组件方法)

项目结构如下:

1.上下文管理:event-contexts.tsx(核心通讯逻辑)

2.页面组件:HomePage.tsxDocsPage.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 的 LinkOutlet 实现路由跳转和页面渲染

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 获取上下文的 statedispatch
  • 展示 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 组件通讯方式对比

  1. Context + useReducer

    • 适用场景:跨组件(多层级)数据共享
    • 优势:避免 props 透传,集中管理状态逻辑
    • 核心 API:createContextuseContextuseReducer
  2. Ref + useImperativeHandle

    • 适用场景:父组件需要直接操作子组件(调用方法 / 获取数据)
    • 优势:直接访问子组件实例,适合控制型交互
    • 核心 API:useRefforwardRefuseImperativeHandle

通过本案例,我们完整实现了从简单到复杂的组件通讯场景,掌握这些钩子的使用方式可以有效解决 React 应用中的数据传递问题。实际开发中,应根据具体场景选择合适的通讯方式,以提高代码的可维护性和扩展性

图例

​编辑

相关推荐
姓王者34 分钟前
chen-er 专为Chen式ER图打造的npm包
前端·javascript
青莲84334 分钟前
Android Jetpack - 2 ViewModel
android·前端
崽崽的谷雨38 分钟前
react里ag-grid实现树形数据展示
前端·react.js·前端框架
栀秋66639 分钟前
就地编辑功能开发指南:从代码到体验的优雅蜕变
前端·javascript·代码规范
国服第二切图仔41 分钟前
Electron for 鸿蒙PC项目实战案例 - 连连看小游戏
前端·javascript·electron·鸿蒙pc
社恐的下水道蟑螂1 小时前
深度探索 JavaScript 的 OOP 编程之道:从基础到进阶
前端·javascript·架构
1_2_3_1 小时前
前端模块联邦介绍
前端
申阳1 小时前
Day 19:02. 基于 SpringBoot4 开发后台管理系统-项目初始化
前端·后端·程序员
学习路上_write1 小时前
FREERTOS_任务通知——使用
java·前端·javascript