7. 组件通信:props
这是什么:props = 父组件传给子组件的数据,只能往下传、只能读;想往回传,就传一个函数下去。
为什么用:上一章拆出的 TaskRow 自己没有任务数据,删除动作也该由持有数据的父组件来做,props 就是这条通道。
如何用:本章从基础 props、children 讲到回调 props,先让子组件喊一声,再由父组件真正把这条任务删掉。
7.1 props 基础
整段可跑:
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
<script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
<script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
<div id='root'></div>
<script type='text/babel' data-presets='react'>
function Greeting({ name, age }) { // 子组件声明「我需要什么」
return <p>{name} 今年 {age} 岁</p>
}
function App() {
return (
<div>
<Greeting name='青蛙' age={11} /> {/* 字符串直接写,JS 表达式加花括号 */}
<Greeting name='熊猫' age={3} />
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>
</body>
</html>
你会看到:「青蛙 今年 11 岁」「熊猫 今年 3 岁」两行。
要点:
- props 只能读,不能改(数据是父的)
- 字符串直接写
name='青蛙',JS 表达式加花括号age={11} - 常用解构
({ name }),可设默认值function G({ page = 1 })
7.2 children:标签中间的内容
整段可跑:
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
<script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
<script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
<div id='root'></div>
<script type='text/babel' data-presets='react'>
function Card({ title, children }) {
return (
<section
style={{ border: '1px solid #ddd', padding: '12px', maxWidth: '360px' }}
>
<h3>{title}</h3>
{children} {/* 承接标签中间的内容(React 版 <slot>) */}
</section>
)
}
function App() {
return (
<Card title='公告'>
<p>任意内容,JSX 全收</p>
<a href='https://react.dev'>链接也行</a>
</Card>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>
</body>
</html>
你会看到:一张带「公告」标题的卡片,里面装着一段文字和一个链接——组合别人写好的 JSX,交给 Card 负责外皮。
7.3 回调 props:子往上报
需求:列表项有「删除」按钮,但数据在父组件——把「删除函数」传下去。整段可跑:
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
<script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
<script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
<div id='root'></div>
<script type='text/babel' data-presets='react'>
function TaskRow({ task, onDelete }) { // 子组件:只负责喊
return (
<li>
{task.text}
<button onClick={() => onDelete(task.id)}>×</button>
</li>
)
}
function TaskList() { // 父组件:负责真操作
const [tasks, setTasks] = React.useState([
{ id: 1, text: '学 React' },
{ id: 2, text: '写教程' }
])
function handleDelete(id) {
setTasks(tasks.filter((t) => t.id !== id)) // 不可变删除
}
return (
<ul>
{tasks.map((t) => (
<TaskRow key={t.id} task={t} onDelete={handleDelete} />
))}
</ul>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<TaskList />)
</script>
</body>
</html>
你会看到:两条任务各带一颗「×」,点哪条删哪条——子组件只调 onDelete(id),真正改数据的始终是父组件。
数据流(和 Vue props/emit 同构):
[TaskList 的 tasks state]
↓ props: task
[TaskRow 显示]
↓ props: onDelete(函数)
[TaskRow 点击 → 调 onDelete(id)]
…… 事情又在父组件发生:setTasks 不可变更新 → 子组件们自动重新渲染
口诀:改数据的永远只有持有者。子组件不持有数据,所以只「递话」,不越权。
7.4 props 穿透问题(先了解)
层级一深 props 就得一层层接力,江湖戏称 props drilling。中级解法(本教程不展开):Context API(内置)或社区状态库(Redux/Zustand)。入门期保持「父管数据」的清晰流即可——三五个组件完全够用。
7.5 动手改
- 给 7.3 的 TaskRow 加「完成」回传
onToggle,父组件不可变更新 done - 在 7.2 的 Card 的 children 里再嵌一层 Card,看渲染
- 给 Greeting 的 age 加默认值 0,故意不传看效果
7.6 小结
- props 单向下行只读,children 接嵌套内容
- 子改父 = 父传函数 + 子调用(对位 Vue 的 emit)
- 数据永远只在一个组件里改;深了切 Context(后续)
下一章:综合练习:React 版待办清单 🐸