前言
在学习 React 的过程中,组件通讯几乎是绕不开的问题。
props 能用、回调能跑、Context 也能生效,但很多时候只是“写对了”,却没真正想清楚为什么要这么写。
其实,组件通讯并不复杂,关键只在两件事上:
这份数据归谁管?又会被哪些组件用到?
想清楚这两点,很多选择会变得很自然:
是直接用 props 传,还是把状态往上提,或是干脆用 Context 做一次广播。
这篇文章不会按用法堆叠,而是沿着这个思路,把几种常见的组件通讯方式放回它们最合适的位置,慢慢讲清楚。
父 → 子(父组件通过 props 传递数据给子组件)
父组件拥有数据,把它以属性(props)形式传给子组件,子组件只能读取。
// ParentA.jsx
import ChildA from './ChildA'
export default function ParentA() {
// 父组件拥有数据
const userName = 'Yvmi'
const userAge = 28
return (
<section>
<h2>父组件(ParentA)</h2>
{/* 把需要的字段明确传下去:可读、不可写 */}
<ChildA name={userName} age={userAge} />
</section>
)
}
// ChildA.jsx
export default function ChildA({ name, age }) {
// 这里只能读取 name 与 age
return (
<div>
<p>子组件接收到了姓名:{name}</p>
<p>子组件接收到了年龄:{age}</p>
</div>
)
}
需要注意的是:子组件只能使用 props,不能直接修改父组件传下来的数据。
三、子 → 父(孩子想告诉父亲一件事)——把“动作”作为 prop
当子组件需要把信息反馈给父组件时,React 的常见做法是:
父组件把一个函数当作 prop 传给子组件,子组件调用它并把数据当参数传回。
就像父组件给子组件一张问卷(函数),子组件把答案交回去,父组件根据答案更新自己的状态。
// ParentB.jsx
import { useState } from 'react'
import ChildB from './ChildB'
export default function ParentB() {
const [counter, setCounter] = useState(0)
// 父亲定义如何处理孩子发来的数据:这是父亲的权利
function handleChildValue(value) {
// 只做自己该做的事:校验 -> 更新状态
const num = Number(value)
if (!Number.isNaN(num)) setCounter(num)
}
return (
<div>
<h2>父组件(ParentB)</h2>
<p>来自子组件的计数:{counter}</p>
<ChildB onSend={handleChildValue} />
</div>
)
}
// ChildB.jsx
export default function ChildB({ onSend }) {
const localValue = 100 // 孩子自己的数据
function send() {
// 孩子决定何时发送,由父亲决定如何处理
onSend(localValue)
}
return <button onClick={send}>把数据发给父亲</button>
}
这里顺带提一下:useState 是 React 提供的一个 Hook,用来在函数组件中保存和更新状态。
在这个例子里,父组件用它来存放“最终的数据”,子组件只是通过调用函数,把值交给父组件处理。
四、兄弟互通:把需要共享的 state 提升到最近的父亲
当两个平级组件要共享同一份信息时,最稳妥的做法是 把这份 state 提升到最近的共同父组件。
ParentC:中转站
// ParentC.jsx
import { useState } from 'react'
import BrotherA from './BrotherA'
import BrotherB from './BrotherB'
export default function ParentC() {
// 由父亲统一拥有这份状态
const [sharedMessage, setSharedMessage] = useState('')
return (
<div>
<h2>父组件(ParentC)- 中转站</h2>
<BrotherA sendToParent={setSharedMessage} />
<BrotherB received={sharedMessage} />
</div>
)
}
BrotherA:发送
// BrotherA.jsx
export default function BrotherA({ sendToParent }) {
return (
<button onClick={() => sendToParent('你好,兄弟!')}>
兄弟 A 发送消息
</button>
)
}
BrotherB:接收
// BrotherB.jsx
export default function BrotherB({ received }) {
return <div>兄弟 B 收到:{received}</div>
}
这比直接互改要好,不会出现多个组件同时试图修改同一份数据引起冲突。
五、跨多层级:家族广播台(Context)——父亲搭一个广播台,孙子随时收听
当需要在深层后代中访问顶层数据,把 props 一层层传下去会很笨拙。这时可以在父亲那儿搭一台“家族广播台”(Context)。
这个广播站里,父组件负责发声,所有后代组件不需要层层传递,只要调到这个频道,就能直接收到同一份信息。
在下面这个示例中,我们为了遵循 React 中的职责分离原则将 FamilyChannel 和 Family 分开处理:
FamilyChannel仅作为数据广播的载体,负责管理和提供全局共享的数据;Family则通过Provider将数据传递给需要的子组件,专注于组件的渲染和 UI 结构。
这种做法让每个组件的职责更加明确,避免了代码的耦合性。FamilyChannel 专注于数据流的管理,而 Family 仅关注 UI 和布局,整体代码结构更加清晰,也让后期的维护和复用变得更加灵活。
FamilyChannel:播放信息的家族广播台
// FamilyChannel.jsx
import { createContext } from 'react'
// 如果外面没有 Provider,才会用到这个默认值(下面有具体解释)
export const FamilyChannel = createContext({ theme: 'light' })
Family:广播台的搭建者
import { FamilyChannel } from './FamilyChannel'
import LevelOne from './LevelOne'
export default function Family() {
const familyData = useMemo(() => ({ theme: 'dark', language: 'zh-CN' }), [])
return (
<FamilyChannel.Provider value={familyData}>
<section>
<h2>父组件(Family) — 我搭了家族广播台</h2>
<LevelOne />
</section>
</FamilyChannel.Provider>
)
}
LevelTwo:孙子听广播,接到信息
// LevelTwo.jsx(孙)
import { useContext } from 'react'
import { FamilyChannel } from './FamilyChannel'
export default function LevelTwo() {
// 这里接收到父组件 Provider 中广播出来的 value
// { theme: 'dark', language: 'zh-CN' }
const { theme, language } = useContext(FamilyChannel)
return (
<div>
<p>孙子组件(LevelTwo)正在接收广播:</p>
<p>主题:{theme}</p>
<p>语言:{language}</p>
</div>
)
}
上面FamilyChannel中的 { theme: 'light' } 只是一个兜底值。
一旦组件被包在 FamilyChannel.Provider 里面,Provider 提供的 value 会 直接覆盖这个默认值,组件将完全拿不到 light。
只有当组件不在任何 Provider 覆盖范围内时,useContext 才会返回这里的默认值。
LevelOne:儿子不听广播,也不妨碍别人听
// LevelOne.jsx(子)
import LevelTwo from './LevelTwo'
export default function LevelOne() {
return (
<div>
<h3>子组件(LevelOne)</h3>
<LevelTwo />
</div>
)
}
LevelOne 只是把 LevelTwo“带进了广播范围”,并没有帮它传话;真正发声的只有 Provider,真正收听的是 LevelTwo。
六、温馨提示(别忘了两个入口文件)
在本地跑这些示例时,别忘了 App.jsx 和 main.jsx 这两个“幕后角色”:一个负责决定台前登场的是哪位父组件,另一个负责把整个应用真正挂到页面上。它们不参与组件通讯的剧情,却始终在背后把舞台撑住。
// App.jsx
// 你可以在这里切换要展示的 demo 父组件
// import ParentA from './demo1/ParentA'
// import ParentB from './demo2/ParentB'
// import ParentB from './demo3/ParentC'
import Family from './demo4/Family' // 展示当前要用的 `Family` 组件
export default function App() {
return <Family />
}
// main.jsx
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')).render(<App />)
最后
组件通讯从来不是在比谁会的 API 多,而是在练一种判断力:
这份数据该由谁掌控,又应该流向哪里。
props、回调、状态提升、Context,本质上都只是不同场景下的选择题。
当你能在画组件树时顺手标出“数据归属”和“使用范围”,答案往往已经写在图里了。
别急着记套路,把这些例子亲手敲一遍,改一改结构、加点日志、看清每一次更新发生的原因。
当你开始觉得「这份数据不该放在这里」的时候,你就真的开始理解 React 了。