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 应用中的数据传递问题。实际开发中,应根据具体场景选择合适的通讯方式,以提高代码的可维护性和扩展性

图例

​编辑

相关推荐
be or not to be11 小时前
HTML入门系列:从图片到表单,再到音视频的完整实践
前端·html·音视频
90后的晨仔11 小时前
在macOS上无缝整合:为Claude Code配置魔搭社区免费API完全指南
前端
沿着路走到底12 小时前
JS事件循环
java·前端·javascript
子春一212 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶12 小时前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn13 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪14 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ14 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied14 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一214 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter