这里我将组件通信分为三类:
- 父传子:上级组件将数据传递给下级组件
- 子传父:下级组件将数据传递给上级组件
- 任意组件:包括父子、祖孙、兄弟组件等在内的任意组件之间的数据通信
一、父传子模式
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 之前,通过 props
将 ref
传递给子组件时,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 的核心思想是 单向数据流 和 组件化 。鼓励开发者通过
props
和state
来管理组件之间的数据和通信,而不是直接依赖事件总线。事件过多也会导致数据传递混乱,造成看起来数据不是单向传递了(实际上还是)。
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>
}