🧭 React 组件通信指南:父传子、子传父、任意组件通信

这里我将组件通信分为三类:

  • 父传子:上级组件将数据传递给下级组件
  • 子传父:下级组件将数据传递给上级组件
  • 任意组件:包括父子、祖孙、兄弟组件等在内的任意组件之间的数据通信

一、父传子模式

1. Props 传递

  • 思路:
    • 父组件定义状态,通过 props 传递给子组件
    • 父组件状态更新,子组件状态随之更新
  • 场景:父子组件传递
  • 缺点:对于多层嵌套组件,数据需要层层传递,代码可维护性差,容易出错
tsx 复制代码
// 父组件
function Parent() {
  const [count] = useState(0)
  return <Child count={count} />
}

// 子组件
function Child({ count }) {
  return <div>{count}</div>
}

2. context 跨层传递

  • 思路:父组件定义全局状态,子孙组件订阅状态,支持 context 透传
  • 场景:简单状态管理,多层组件跨层传递,全局状态共享
  • 缺点:不适合复杂状态的管理,存在非必要更新问题,拆分状态又存在嵌套地狱问题
tsx 复制代码
import { createContext, useState, use } from 'react'

const Context = createContext(0)

export default function Parent() {
  const [count, setCount] = useState(0)
  return (
    <Context.Provider value={count}>
      <Child increase={() => setCount(c => c + 1)}/>
    </Context.Provider>
  )
}

function Child({ increase }) {
  return (<>
    <button onClick={increase}>click</button>
    <GrandChild />
  </>)
}

function GrandChild() {
  const count = use(Context)
  return <div>{count}</div>
}

二、子传父模式

1. callback 回调函数

  • 思路:子组件调用父组件传递的回调函数,将数据传递给父组件
  • 场景:子组件主动触发,传递数据到父组件
tsx 复制代码
// 父组件
function Parent() {
  const handleChildUpdate = (data) => {
    console.log('Received:', data);
  };
  
  return <Child onUpdate={handleChildUpdate} />;
}

// 子组件
function Child({ onUpdate }) {
  return <button onClick={() => onUpdate(Date.now())}>Send</button>;
}

2. ref(reference) 引用

  • 思路:父组件创建 ref,通过 ref 获取子组件引用,调用子组件方法
  • 场景:父组件触发,调用子组件方法,获取子组件数据
  • 缺点:破坏组件结构

要将子组件数据传递到父组件,还有一种思路是直接调用子组件实例上的方法。re(reference) 是 React 中特殊的变量,可以存储可变值(ref.current is mutable),ref 的修改不会引起组件重新渲染,可以用于获取子组件实例。ref 在类组件和函数组件上用法不同,且在 react19 中在函数组件上的使用得到了强化。

2.1 class ref(类组件)

类组件可以直接用 ref 拿到组件实例,然后调用子组件的方法。

tsx 复制代码
export default class Parent extends React.Component {
  childRef: React.RefObject<Child>
  constructor(props) {
    super(props)
    this.childRef = React.createRef()
  }
  
  getChildData = () => {
    const data = this.childRef.current.sendMsg()
    console.log('Received:', data)
  }
  
  render(): React.ReactNode {
    return (<>
      <button onClick={this.getChildData}>get child data</button>
      <Child ref={this.childRef}/>
    </>)
  }
}

class Child extends React.Component {
  sendMsg = () => 'data from Child'
  render() {
    return <></>
  }
}

2.2 forwardRef + useImperativeHandle(函数组件)

forwardRef 是 React 提供的一个高阶函数,它允许组件将 ref 转发到其内部的 DOM 节点或子组件。而 useImperativeHandle 是 React 提供的一个 Hook,允许自定义通过 ref 暴露给父组件的实例值或方法。

tsx 复制代码
export default function() {
  const childRef = useRef(null)

  const getChildData = () => {
    const data = childRef.current.sendMsg()
    console.log('Received:', data)
  }

  return (<>
    <button onClick={getChildData}>get child data</button>
    <Child ref={childRef} />
  </>)
}

const Child = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    sendMsg: () => 'data from Child'
  }))
  return <></>
})

2.3 ref as prop(react19)

React 19 之前,通过 propsref 传递给子组件时,ref 不会被自动绑定到子组件的 DOM 元素,因此需要使用 forwardRef 做一层转发。forwardRef 是 React 提供的高阶函数,用于将 ref 转发到子组件的 DOM 元素或组件实例。React 19 增强了 ref 作为 props 的能力,现在可以直接将 ref 作为 props 传递,而不再需要 forwardRef

tsx 复制代码
// 父组件
export default function Parent() {
  const childRef = useRef(null)

  // 调用子组件方法
  const getChildData = () => {
    const data = childRef.current.handle()
    console.log('Received:', data)
  }

  return (<>
  <button onClick={getChildData}>get data from child</button>
    <Child ref={childRef}/>
  </>)
}

// 子组件
function Child({ ref }) {
  // 将方法挂载到 ref 上
  ref && (ref.current = { handle: () => 'data from child' })
  return <></>
}

三、任意组件通信

包括父子、祖孙、兄弟组件等在内的 任意组件,都可以用 "共享全局状态+订阅更新机制" 的思想来实现组件通信。任意组件之间只要共享了同一数据状态源,就能进行数据通信。基于这个思路,可以想到包括 context、reducer、状态提升(lift state up)、事件总线机制(event bus)、外部状态管理库等都能实现组件通信。

1. Context

React Context 是一种用于在组件树中共享数据的机制。它允许你将数据从父组件传递到深层嵌套的子组件,而无需通过 props 逐层传递。Context 特别适合用于全局数据(如主题、用户信息、语言设置等)的共享。

1.1 context(函数组件)

  • 函数组件通过 Reatc.createContext 创建 context,通过 Context.Provider 通过 useContext 或者 Consumer 来消费 context
  • React19 支持直接使用 Context 作为 Provider,支持使用 use 消费 context
tsx 复制代码
const CounterContext = createContext({ count: 0, add: () => {} })

export default function App() {
  const [count, setCount] = useState(0)
  return (
    // react19 支持直接使用 context 作为 provider
    <CounterContext value={{ count, add: () => setCount(c => c + 1) }}>
      <ComponentA />
      <ComponentB>
        <ComponentC />
      </ComponentB>
    </CounterContext>
  )
}

// useContext 消费
function ComponentA() {
  const { count } = useContext(CounterContext)
  return (<>
    <div>ComponentA:{count}</div>
  </>)
}

// react19,use 消费
function ComponentB({ children }) {
  const { add } = use(CounterContext)
  return (<>
    <button onClick={add}>+</button>
    { children }
  </>)
}

// 多层嵌套组件消费
function ComponentC() {
  const { count } = use(CounterContext)
  return (<div>ComponentC:{count}</div>)
}

1.2 context(类组件)

类组件通过 Reatc.createContext 创建 context,通过组件绑定 context 的形式或者 Consumer 来消费 context。

tsx 复制代码
// 创建 Context
type CounterContextType = { count: number, increase: () => void }
const CounterContext = createContext<CounterContextType>({ count: 0, increase: () => {} })

// 父组件
type ParentComponentPropsType = Omit<CounterContextType, 'increase'>
export default class ParentComponent extends Component {
  state: Readonly<ParentComponentPropsType> = {
    count: 0
  }
  
  render() {
    return (
      <CounterContext
        value={{
          count: this.state.count,
          increase: () => this.setState({ count: this.state.count + 1})
        }}
      >
        <ChildComponent />
      </CounterContext>
    )
  }
}

// 子组件
class ChildComponent extends Component {
  // 绑定 Context
  static contextType = CounterContext
  render() {
    // 访问 Context 数据
    const { count, increase } = this.context as CounterContextType
    return <div onClick={increase}>{count}</div>
  }
}

2. URL 参数通信

使用 URL 参数通信 也是一种常见的组件通信方式,尤其适用于需要在页面之间传递数据的场景。它的核心原理是通过 路由 将数据编码到 URL 中,然后在目标组件中解析 URL 参数以获取数据。要使用路由通信,我们可以在 React 项目中引入 React-Router,或者直接使用内置集成了 App Router 的 Next.js 框架。

由于路由信息是浏览器天然支持的全局状态,下面这些功能都可以在各个组件中使用。

  • 路径参数:如 /user/:id
  • 查询参数:如 /user?id=123
  • 嵌套路由:支持多级路由嵌套
  • 编程式导航:通过代码控制页面跳转

例如,点击用户信息查看,用户id可通过路由查询参数(?name=John&age=25)传递到详情组件中,可以进一步查询后台数据。

tsx 复制代码
import React from "react"
import { useSearchParams } from "react-router-dom"

function UserDetail() {
  // 解析查询参数
  const [searchParams] = useSearchParams();
  const name = searchParams.get("name");
  const age = searchParams.get("age");

  return (
    <div>
      <h1>User Detail</h1>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

export default UserDetail

3. EventBus 事件总线

  • 思路:事件总线是典型的发布订阅机制,注册事件的回调函数,触发对应事件时,订阅了该事件的地方都会执行回调函数。
  • 场景:父子、祖孙、兄弟组件等需要数据通信的组件,通过订阅和触发事件进行数据通信。

在React 开发中此方法一般不常用,只是借鉴思想。React 的核心思想是 单向数据流组件化 。鼓励开发者通过 propsstate 来管理组件之间的数据和通信,而不是直接依赖事件总线。事件过多也会导致数据传递混乱,造成看起来数据不是单向传递了(实际上还是)。

1、定义事件总线

ts 复制代码
type IFunction = (...args: unknown[]) => void

class EventBus {
  /**
   * 事件集合
   */
  private listeners: Record<string, IFunction[]> = {}

  /**
   * 注册事件
   * @param event
   * @param callback
   */
  on(event: string, callback: IFunction) {
    if (!this.exist(event)) {
      this.listeners[event] = []
    }
    this.listeners[event].push(callback)
  }  

  /**
   * 注销事件
   */
  off(event: string, callback: IFunction) {
    if (!this.exist(event)) return
    this.listeners[event] = this.listeners[event].filter(cbk => cbk != callback)
  }

  /**
   * 触发事件
   * @param event
   * @param args
   */
  emitt(event: string, ...args: unknown[]) {
    if (!this.exist(event)) return
    const callbacks = this.listeners[event]
    callbacks.forEach(callback => {
      callback(...args)
    })
  }

  /**
   * 校验事件是否已注册
   * @param event
   * @returns
   */
  private exist(event: string) {
    return !!this.listeners[event]
  }
}

/**
 * 全局事件总线
 */
export const msgEvent = new EventBus()

2、在组件中使用

如下所示,在组件 B 中订阅了事件,在父组件 A 和兄弟组件 C 中触发了事件,更新状态。

tsx 复制代码
import { useEffect, useState } from "react"
import { msgEvent } from "./EventBus"

// 事件名
const EVEN_NAME = 'send-msg'

export default function A() {
  // 触发事件
  const setMsg = () => msgEvent.emitt(EVEN_NAME, 'Hello from A')
  return (<>
    <button onClick={setMsg}>A send message</button>
    <B />
    <C />
  </>)
}

function B() {
  const [msg, setMsg] = useState('')
  const handleMsg = (message: string) => setMsg(message)
  
  useEffect(() => {
    // 订阅事件
    msgEvent.on(EVEN_NAME, handleMsg)
    // 注销事件
    return () => msgEvent.off(EVEN_NAME, handleMsg)
  }, [])
  
  return <p>B get message: {msg}</p>
}

function C() {
  // 触发事件
  const setMsg = () => msgEvent.emitt(EVEN_NAME, 'Hello from C')
  return <button onClick={setMsg}>C send message</button>
}

4. Zustand 等状态管理库

除了 React 内置的状态管理 API 外,还有很多状态管理库比如 Redux/Valtio/Jotai/Zustand 等,状态管理本质就是管理组件之间的状态通信。这里以 Zustand 为例,结束本文。

1、定义状态管理 hook:useCounterStore

ts 复制代码
import { create } from 'zustand'

type CounterStore = {
  count: number,
  doubleCount: () => number,
  increase: () => void,
  increment: () => void,
  update: (newCount: number) => void
}

/**
 * 创建一个 hook:useCounterStore
 */
export const useCounterStore = create<CounterStore>((set, get) => {
  return {
    count: 0,
    doubleCount: () => get().count * 2,
    increase: () => set(state => ({ count: state.count + 1 })),
    increment: () => set((state) => ({ count: state.count + 1 })),
    update: (newCount) => set({ count: newCount })
  }
})

2、在组件中使用

tsx 复制代码
import { useCounterStore } from './store'

export default function Parent() {
  return (<>
    <ChildA />
    <ChildB />
  </>)
}

function ChildA() {
  // 组件 A 修改数据
  const { increase } = useCounterStore()
  return <button onClick={increase}>increase</button>
}

function ChildB() {
  // 组件 B 消费数据
  const { count, doubleCount } = useCounterStore()
  return <div>
    <div>count: {count}</div>
    <div>double count: {doubleCount()}</div>
  </div>
}
相关推荐
沉默璇年31 分钟前
使用 pnpm 安装依赖包后,如果将依赖包直接复制内网环境中,可能会出现无法使用的情况,且不能联网下载,如何解决?
前端
数字供应链安全产品选型33 分钟前
SBOM风险预警 | 恶意NPM组件开展木马投毒攻击,目标针对国内泛互企业
前端·npm·node.js
web1359560970534 分钟前
【JavaEE】Spring Web MVC
前端·spring·java-ee
williamdsy1 小时前
【JavaScript】记录一个奇怪的问题,前端一次提交注册,后端收到两次接口调用,网络只显示一个register请求
开发语言·前端·javascript
sorryhc1 小时前
前端OOM内存泄漏如何排查?
前端·javascript·性能优化
多喝热水2341 小时前
HTML(超文本标记语言)
前端·html
看晴天了1 小时前
uniapp学习
前端
竺梓君2 小时前
ref和reactive实现原理剖析
前端·javascript·vue.js
木木黄木木2 小时前
html5-Canvas弹跳小球项目开发总结
前端·html·html5
爱喝羊奶2 小时前
数组全解
前端·javascript