React 父子组件传值有 3 种方式:父传子用 props,把数据写在子组件标签的属性上;子传父用回调函数,父组件先把函数当 props 传下去,子组件调用它把数据带回来;跨多层级用 Context,让任意深度的后代组件直接取用。三种方式都建立在同一个铁律之上——单向数据流。
刚学 React 的人最容易犯两个错误:一是试图在子组件里直接修改 props,二是让父子各存一份同样的数据,结果两边对不上。今天这篇文章,编程狮就把这 3 种传值方式一次讲清:从最小可运行示例开始,重点给出子传父的完整实战写法,最后说明 Context 到底该在什么时候才值得引入。

一、父子组件传值到底在传什么
React 里的数据是单向流动的:只能从父组件流向子组件,不能反向流动。这条规则叫单向数据流,是整个 React 组件通信的地基。
所谓组件传值,其实涵盖三种场景:
- 父传子 → 用 props,父组件把数据写成子组件标签的属性;
- 子传父 → 用回调函数,父组件先传一个函数下去,子组件调用它把数据交回来;
- 跨层级 → 用 Context,祖组件提供数据,任意深度的后代直接取用。
理解这三者的关键,是先接受一个事实:子组件拿到的 props 是只读的,它没办法、也不应该直接修改。
1.1 一个能立刻跑起来的最小示例
function Welcome({ name }) {
return <p>你好,{name}</p>;
}
export default function App() {
return <Welcome name="小明" />;
}
父组件在使用子组件时,把数据写成标签属性 name="小明";子组件用解构的方式把它接住。整条链路是单向的:数据从 App 流向 Welcome,Welcome 只是负责展示。
1.2 常见误解
误解一:以为子组件可以直接改 props。 不行。props 是只读的,在子组件里写 props.name = '小红' 不会生效。想改变数据,必须由持有那份状态的组件来改。
误解二:以为父子应该各存一份同样的数据。 这是"状态重复"的经典错误,同一份数据存两处迟早会对不上。正确做法是状态提升——把状态放到最近的公共父组件里,父子共用一份。
误解三:以为 Context 更先进,应该优先用。 恰恰相反。Context 会让组件的复用性变差,数据来源也更难追踪。相邻层级之间用 props 完全够了。
二、父传子:用 props 单向传递
props 的用法有个小细节:字符串可以直接用引号,其他 JavaScript 值必须放进花括号。
function UserCard({ name, age, tags, children }) {
return (
<div>
<h3>{name}</h3>
<p>年龄:{age}</p>
<p>标签:{tags.join('、')}</p>
<div>{children}</div>
</div>
);
}
export default function App() {
const tags = ['前端', 'React'];
return (
<UserCard name="小明" age={18} tags={tags}>
<em>这是通过 children 传入的内容</em>
</UserCard>
);
}
几个要点:
第一,name="小明" 传的是字符串,age={18} 花括号里是 JavaScript 表达式,传的是数字 18。如果漏掉花括号写成 age="18",子组件收到的就是字符串 "18",做数值运算时会出问题——这是新手最常踩的一个坑。
第二,props 可以传任意 JavaScript 值,包括数组、对象、函数,甚至 JSX。
第三,children 是个特殊的 props,接收的是写在开始标签和结束标签之间的内容,做布局容器类组件时非常方便。
第四,props 只读。如果子组件需要一份"自己能改"的数据,可以用 useState 自己维护,但初始值可以来自 props。需要特别提醒的是,不要用一个 state 去"复制" props——那样父组件更新时,这个 state 不会跟着变,数据就僵住了。基于 props 派生新值,直接算就行。
props 的完整能力远不止传数据,默认值、类型校验、展开写法都有一套约定,想系统过一遍可以顺着 React 教程 的组件章节往下读。
三、子传父:用回调函数把数据带回来
这是父子组件通信里最核心、也最容易绕晕的一节。思路其实只有一句话:父组件定义一个函数,把它当 props 传给子组件;子组件在合适的时机调用这个函数,并把自己的数据作为参数带进去。
来看一个完整的实战例子——父组件要实时拿到搜索框里的关键字:
import { useState } from 'react';
// 子组件:只负责展示和收集输入,自己不持有数据
function SearchBox({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="输入关键字"
/>
);
}
// 父组件:持有状态,并把修改状态的函数传给子组件
export default function App() {
const [keyword, setKeyword] = useState('');
return (
<div>
<SearchBox value={keyword} onChange={setKeyword} />
<p>父组件收到的关键字:{keyword}</p>
<p>字符数:{keyword.length}</p>
</div>
);
}
把这条链路拆开看,一共四步:
- 父组件用 useState 持有 keyword,这是整份数据的唯一来源;
- 父组件把 keyword 通过 value 传下去显示,同时把 setKeyword 通过 onChange 传下去当回调;
- 用户输入时,子组件调用 onChange(e.target.value),实际上就是在调用父组件的 setKeyword;
- 父组件状态更新后重新渲染,子组件拿到新的 value,界面同步刷新。
这就是所谓的状态提升:把状态放到父组件,子组件变成受控组件。数据始终只有一份,父子永远对得上。
如果回调需要带上子组件自己的参数,要用箭头函数包一层。 比如列表项点击删除时,要把自己的 id 交回父组件:
function TodoItem({ text, onDelete }) {
return (
<li>
{text}
<button onClick={onDelete}>删除</button>
</li>
);
}
// 父组件使用时包一层,把 id 带过去
<TodoItem text="写笔记" onDelete={() => handleDelete(item.id)} />
注意这里的区别:写 onClick={onDelete} 是直接把函数交给按钮,点击时不带任何参数;写 onClick={() => handleDelete(item.id)} 才是调用时带上自己的 id。这个写法差异是子传父时最高频的绊脚石。
四、跨层级传值:用 Context 避免层层透传
当组件层级很深时,用 props 一层层往下传会非常痛苦:中间的组件明明不用这份数据,却不得不接收它再转手传下去。这个现象叫 props 透传,Context 就是为它准备的。
用法分三步:创建、提供、消费。
import { createContext, useContext, useState } from 'react';
// 第一步:创建 Context,参数是默认值
const ThemeContext = createContext('light');
// 消费方:任意深度的后代组件都能直接取用
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>当前主题:{theme}</button>;
}
// 中间层组件完全不需要知道主题的存在
function Toolbar() {
return <ThemedButton />;
}
// 第二步:用 Provider 提供数据
export default function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</ThemeContext.Provider>
);
}
要点有三个。第一,createContext 的参数是默认值,只有在组件外层找不到匹配的 Provider 时才会用到。第二,Provider 的 value 一旦变化,所有消费它的组件都会重新渲染,所以不要把频繁变动的大对象随手塞进去。第三,注意看 Toolbar——它既没有接收 theme,也没有往下传,这正是 Context 的价值所在:跨过了中间那一层。
需要提醒的是,Context 不是全局变量的替代品。它会让数据的来源变得不那么直观,调试时也更难追。只有当同一份数据确实需要被多层级的组件共享时,才值得引入。
五、怎么选:父子组件 3 种传值方式清单
| 场景 | 用什么 | 说明 |
|---|---|---|
| 父组件给直接子组件数据 | props | 默认选择,链路清晰可追踪 |
| 子组件要把数据交回父组件 | 回调函数 | 父传函数下去,子组件调用 |
| 隔了很多层共享同一份数据 | Context | 避免 props 层层透传 |
| 兄弟组件之间要互通 | 状态提升 | 把状态放到共同父组件 |
再补三条原则:
第一,先想清楚这份状态该归谁管,把它放在需要它的组件的最近公共父组件里,这就是状态提升。兄弟组件互通其实也是这个思路——不是让两个兄弟直接对话,而是让它们的父亲统一调度。
第二,子组件永远不要直接改 props,想修改就调用父组件传下来的函数。
第三,不要一上来就用 Context。多数应用里大部分数据用 props 和回调就够了。如果确实出现了全局共享、频繁变化的大量状态,再去评估专门的状态管理方案,但那是另一个话题了。
React 的组件模型建立在 JavaScript 之上,想把解构、箭头函数这些基础打牢,可以顺着 JavaScript 教程 系统补一遍。

总结
React 父子组件传值就三句话:父传子用 props,子传父用回调函数,跨多层级用 Context。
- 单向数据流是铁律,子组件不能改 props,想改就调用父组件传下来的函数;
- 子传父的实战套路是状态提升:状态放父组件,子组件做受控组件;
- 回调要带参数时用箭头函数包一层,
onClick={fn}和onClick={() => fn(id)}不是一回事; - Context 用于解决跨层级的 props 透传,不要拿来当全局变量用。
延伸学习
- React 入门配套课程 —— 从组件与 props 讲到状态管理,配合本文示例一起练。
- React 父子组件互调笔记 —— 把 ref 调用子组件方法等进阶场景一次列清。
- 在线运行 JavaScript 代码工具 —— 不搭环境也能验证本文的解构与回调函数写法。
常见问题
Q:子组件能不能直接修改 props?
A:不能。props 是只读的,在子组件里给它赋值既不会生效,也违背了单向数据流的设计。如果子组件需要"能改"的数据,正确做法是让父组件把状态和修改状态的函数一起传下来,子组件通过调用函数去改。如果这份数据只有子组件自己关心,那就用 useState 在子组件内部维护。
Q:父组件怎么知道是哪个子组件触发的操作?
A:让子组件在调用回调时把自己的标识传回来。比如列表渲染时写成 onDelete={() => handleDelete(item.id)},用箭头函数把当前项的 id 包进去,父组件的 handleDelete 收到 id 就能分辨来源。如果直接用 onDelete={handleDelete},事件对象会作为第一个参数传入,你就拿不到子组件的 id 了。
Q:Context 和 props 到底该选哪个?
A:优先用 props。只有当同一份数据需要被相隔多层、且中间层组件完全用不到它的后代组件共享时,才引入 Context。Context 会让数据来源变得隐式,组件也变得更难复用和调试。一个简单的判断标准:如果需要连着传三层以上、且中间层只是"过路",就该考虑 Context 了。
Q:两个兄弟组件之间怎么传值?
A:兄弟组件之间不能直接通信,正确做法是状态提升:把共享的状态放到它们最近的共同父组件里,父组件通过 props 把数据分别发给两个兄弟,同时把修改函数也传下去。某个兄弟调用函数改了状态,父组件重新渲染,另一个兄弟自然就收到了新数据。这也是单向数据流最典型的体现。

免费 AI IDE



