
一句话:Props 是父组件传给子组件的只读数据,它是「单向数据流」的载体——子组件不能修改 props,只能通过调用父组件传下来的回调来请求变更。
一、Props 的本质
// 组件接收 props 就是一个对象
function UserCard(props) {
console.log(props); // { name: '张三', age: 18, children: [...] }
return {props.name};
}
// 用解构更清晰
function UserCard({ name, age, children }) {
return (
{name}
{age}
{children}
);
}
// 使用(JSX 属性会组成 props 对象)
内容
JSX 与编译产物的对应关系:
内容
↓
jsx(UserCard, { name: '张三', age: 18, children: '内容' })
↓
UserCard({ name: '张三', age: 18, children: '内容' })
↑ 组件本质上就是「用 props 对象调用的函数」
二、为什么 Props 是只读的
// ❌ 不能修改 props
function Child({ user }) {
user.name = '李四'; // 违反约定(而且对基础类型根本无效)
return {user.name};
}
// ✅ 需要改数据时:通过回调请求父组件更新
function Child({ user, onChange }) {
return ;
}
function Parent() {
const [user, setUser] = useState({ name: '张三' });
return setUser({ ...user, name })} />;
}
为什么必须只读(三个理由):
| 理由 | 说明 |
|---|---|
| 数据流向可预测 | 数据只从父到子,任何一个值的来源唯一,便于调试 |
| 避免状态不同步 | 如果子组件能改 props,父组件的 state 与子组件的显示会不一致 |
| 让更新可追踪 | 状态变更集中在「拥有该状态的组件」里,容易排查问题 |
注意一个反直觉的点:如果 props 是对象或数组,在子组件里修改它的属性(
user.name = 'x')在技术上是「生效」的(因为传递的是引用),React 也不会报错。但这破坏了单向数据流,是明确的错误做法——因为父组件不知道数据被改了,下次渲染又会被覆盖,行为难以预测。
三、默认值
// ✅ 函数组件:用「默认参数」(推荐)
function Greeting({ name = '访客', age = 0 }) {
return {name} {age};
}
// ⚠️ 对象/数组作为默认值时,注意「每次渲染都是新对象」
function List({ items = [], onSelect = () => {} }) {
// ⚠️ 默认的 [] 与 () => {} 每次渲染都是新引用
// → 如果有依赖它们的 useEffect / useMemo,会每次触发
}
// ✅ 更稳妥:把默认值提到模块作用域
const EMPTY_ARRAY = [];
const noop = () => {};
function List2({ items = EMPTY_ARRAY, onSelect = noop }) {
// 引用稳定 ✓
return null;
}
// ❌ defaultProps 已被废弃(函数组件上会警告,React 19 移除)
Greeting.defaultProps = { name: '访客' };
// ✅ 用默认参数代替
function Greeting2({ name = '访客' }) {}
| 写法 | 状态 | 建议 |
|---|---|---|
默认参数 ({ name = 'x' }) |
✅ 推荐 | 函数组件标准写法 |
defaultProps |
❌ 已废弃(React 18 警告、19 移除) | 不要用 |
undefined 触发默认值 |
✅ | 传 null 不会触发默认值 ⚠️ |
function Comp({ value = 'default' }) {
return {value};
}
{/* 'default' */}
{/* 'default'(undefined 触发默认值) */}
{/* 渲染空(null 不触发默认值)⚠️ */}
{/* 空字符串(不触发默认值) */}
四、children 的几种形态
// ① 单个元素
内容
// children: 内容
// ② 多个元素
标题
内容
// children: [,
]
// ③ 纯文本
纯文本
// children: '纯文本'
// ④ 函数(渲染函数 / render props)
{(data) => {data}}
// children: (data) =>
{data}
// ⑤ 什么都不传
// children: undefined
// tips: children 的类型
type Props = {
children: React.ReactNode; // ⭐ 最常用:任何可渲染的内容
};
type Props2 = {
children: React.ReactElement; // 只允许单个元素(不允许文本/数组)
};
type Props3 = {
children: (data: Data) => React.ReactNode; // 渲染函数
};
// React.ReactNode 的定义(简化)
type ReactNode =
| ReactElement
| string
| number
| boolean
| null
| undefined
| Iterable
| ReactPortal;
五、展开传递与透传
// ① 展开传递(把对象的属性一次性传给子组件)
const user = { name: '张三', age: 18, avatar: 'a.png' };
// 等价于 name={user.name} age={user.age} avatar={user.avatar}
// ② 有选择地传递
const { password, ...safe } = user; // 剔除敏感字段再传
// ③ 覆盖(后面的生效)
// name 变成「李四」
// ④ 透传其余 props(组件库常见模式)
function Input({ label, ...rest }: InputProps) {
return (
);
}
// ⑤ 展开传递与 key 的关系
const list = [{ id: 1, name: 'a' }];
{list.map((item) => )}
// ⚠️ 如果 item 里也有 key 属性,会被后面的 {...item} 覆盖
// ✅ 把 key 放在最后,或确保数据里没有同名字段
{list.map((item) => )}
六、多个「插槽」的写法(React 没有 slot)
// React 用 props 传元素来实现「具名插槽」
interface LayoutProps {
header: React.ReactNode;
sidebar?: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
}
function Layout({ header, sidebar, children, footer }: LayoutProps) {
return (
{header}
{sidebar && }
{children}
{footer && }
);
}
// 使用(比 Vue 的 更「JS 化」)
}
sidebar={ }
footer={}
>
| 方案 | 写法 | 特点 |
|---|---|---|
children |
<Card>内容</Card> |
单一插槽,最简洁 |
| 具名 props | header={<Header/>} |
多插槽,类型清晰 |
| 渲染函数 | renderItem={(x) => <Item/>} |
把「怎么渲染」交给使用方 |
children 为函数 |
<Card>{(d) => ...}</Card> |
同 render props,但要处理非函数的情况 |
七、Props 的类型约束(TypeScript)
// ① 基本类型定义
interface UserCardProps {
name: string;
age?: number; // 可选
onClick?: () => void;
children?: React.ReactNode;
}
function UserCard({ name, age = 0, onClick, children }: UserCardProps) {
return {name}{age}{children};
}
// ② 事件的类型
interface FormProps {
onChange: (value: string) => void; // 自定义回调(推荐)
onSubmit: (e: React.FormEvent) => void; // 原生事件
onClick: React.MouseEventHandler;
}
// ③ 原生元素属性的扩展(组件库常见)
interface ButtonProps extends React.ButtonHTMLAttributes {
variant?: 'primary' | 'secondary';
}
function Button({ variant, ...rest }: ButtonProps) {
return ;
}
; // 原生属性都能用
// ④ 联合类型约束(枚举式 props)
interface AlertProps {
type: 'success' | 'error' | 'warning';
message: string;
}
// ⑤ 泛型组件(容器组件常用)
interface ListProps {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List({ items, renderItem, keyExtractor }: ListProps) {
return {items.map((it, i) => - {renderItem(it, i)}
)}
;
}
// 使用时会自动推断 T
u.name} keyExtractor={(u) => u.id} />;
// ⑥ 互斥 props(要么传 a 要么传 b)
type Props =
| { variant: 'link'; href: string }
| { variant: 'button'; onClick: () => void };
八、常见坑
// ① 修改 props(违反单向数据流)
function Child({ list }) {
list.push('x'); // ❌ 直接改了父组件的数组
}
// ② 依赖 props 对象引用,导致每次渲染都触发副作用
function Comp({ options = {} }) {
useEffect(() => {
// options 每次渲染都是新对象 → 每次都会执行
}, [options]);
}
// ③ 传 null 不会触发默认值
function Comp2({ value = 'default' }) {}
// 渲染空,不是 'default'
// ④ 把对象直接渲染
{user} // ❌ Objects are not valid as a React child
// ⑤ 展开传递时 key 被覆盖
// ⚠️ item.key 会覆盖
// ⑥ 层层透传 props(props drilling)
// ⚠️ 中间层不需要但必须传 → 用 Context
// ⑦ 在子组件里深拷贝 props 再修改(性能与语义双重问题)
const local = { ...props.user }; // 浅拷贝,嵌套对象仍是共享引用
// ⑧ 用下划线命名 props(不符合 JSX 习惯)
// ⚠️ 应该用 userName
面试延伸
- 「Props 为什么是只读的?」
这是「单向数据流」的核心约束。理由有三:① 数据流向可预测——任何值的来源唯一,调试时不需要追「谁改了它」;② 避免状态不同步——如果子组件能改 props,父组件的 state 与子组件显示会不一致;③ 让更新可追踪——状态变更集中在「拥有该状态的组件」里。需要改数据时,正确做法是「父组件把回调传下去,子组件调用回调请求变更」。
- 「子组件想修改父组件的数据怎么办?」
标准模式是「状态提升 + 回调下传」:把状态放在两者最近的公共父组件里,父组件把「修改函数」作为 props 传给子组件,子组件在需要时调用它。如果层级很深,用 Context 避免层层透传;如果是全局共享的数据,用状态管理库(Zustand/Redux)。
- 「为什么
defaultProps被废弃了?」
因为「默认参数」是语言原生能力,语义更清晰、更符合函数组件的直觉,而且 defaultProps 在函数组件上有额外开销(每次渲染都要合并对象)且难以做类型推导。React 18 对函数组件上的 defaultProps 发出警告,React 19 已移除。类组件仍然支持 defaultProps。
- 「传
null和传undefined在 props 上有什么区别?」
关键区别在默认值:undefined 会触发默认参数(value = 'default' 生效),而 null 不会(渲染出空)。另外在类型层面,如果 props 的类型是 string,传 null 会类型报错(除非类型里包含 null)。所以「想表达『没传值』」应该用 undefined。
- 「React 有 slot 吗?怎么做多插槽?」
没有 slot 概念。多插槽用「传 props 元素」实现:<Layout header={<Header/>} footer={<Footer/>}>{children}</Layout>。相比 Vue 的具名 slot,这种方式类型更清晰(可以在接口里精确约束每个插槽的类型),但也更「JS 化」,需要显式写 JSX。另一种形式是渲染函数(render props):把「怎么渲染」交给使用方,如 renderItem={(item) => <Item/>}。
- 「为什么说『层层透传 props』是问题?」
因为中间组件「不需要这个数据却必须传」,导致:① 中间组件被迫知道与自己无关的接口(耦合);② 改一个 prop 名要改整条链路;③ 重构困难。解法是 Context(适合跨层级共享的「主题、用户、语言」)或状态管理库(适合频繁变化或结构复杂的全局状态)。但要注意 Context 有重渲染代价,不是所有透传都该改成 Context(两三层透传其实还好)。
- 「props 是对象,会不会有引用问题?」
会,而且这是 React 性能问题的主要来源之一。因为 {...}、[]、() => {} 在每次渲染时都是新引用,如果子组件用了 React.memo,比较会失败导致重渲染;如果作为 useEffect 依赖,会每次触发。解法:① 把常量提到模块作用域;② 用 useMemo / useCallback 稳定引用;③ 对「只传固定内容」的场景用 children 或 renderXxx 的模式减少依赖。
- 「TypeScript 里怎么写组件的 props 类型?」
三种常见形式:① 接口(interface Props { ... })——适合大多数场景,报错信息友好;② 类型别名 + 联合——适合「互斥 props」这类复杂约束;③ 扩展原生属性(extends React.ButtonHTMLAttributes<HTMLButtonElement>)——做基础组件时必须,这样 onClick、disabled、className 等原生属性都能透传且有类型。children 一般用 React.ReactNode。
一句话速记
Props 是只读的单向数据流(子组件要改数据 → 回调 + 状态提升);默认值用默认参数(
defaultProps已废弃),undefined触发默认值、null不触发;children只是普通 props 字段,多插槽用「传元素 props」或 render props;{...obj}展开时小心覆盖key;React 没有 slot,层层透传要改用 Context(但两三层其实可以忍);TS 里基础组件记得extends React.XxxHTMLAttributes。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!