Props 完全指南
一句话定义:Props 就是组件函数的参数。
组件函数是什么?就是用 function 声明、大写字母开头、返回 JSX 的函数——React 拿它当”UI 零件”来用。详细区别见 React组件与函数本质。
// 这就是一个组件函数
function UserCard(props) {
// 大写开头 + 接收 props + 返回 JSX
return <div>{props.name}</div>;
}
一、从函数视角看 Props
// 普通函数
function greet(name) {
return `Hello, ${name}`;
}
greet("小白");
// React 组件
function GreetCard(props) {
return <div>Hello, {props.name}</div>;
}
<GreetCard name="小白" />;
| 普通函数 | React 组件 |
|---|---|
定义:function fn(arg1, arg2) | 定义:function Comp(props) |
调用:fn('a', 'b') | 调用:<Comp arg1="a" arg2="b" /> |
关键区别:普通函数参数是”散装”的,React 组件参数永远是”打包”成对象的。
// <Comp name="Jack" age={18} /> 等价于
const props = { name: "Jack", age: 18 };
Comp(props);
二、为什么叫 “Props”?
Props 是 Properties(属性)的缩写,可以理解为组件的”配置单”。
类似 HTML 中的 <img src="photo.jpg" alt="照片" />,React 组件可以传字符串、数字、函数、对象,甚至其他组件。
三、Props 为什么要用对象模式?
React 官方文档明确说:“props are the only argument to your component”。为什么只用一个对象,而不是像普通函数那样传多个参数?
1. 方便数据透传
场景:组件库中需要把属性透传给底层原生标签。
// 对象模式:一行代码搞定所有属性透传
function MyInput(props) {
return <input {...props} />;
}
// 使用:无论传10个还是20个属性,都不用改组件内部
<MyInput placeholder="请输入" type="text" className="red" />
// 如果是多参数模式 ❌ 灾难现场
function MyInput(placeholder, type, className, value, onChange, onBlur...) {
return <input placeholder={placeholder} type={type} className={className} ... />;
}
// 每加一个新属性就得改函数定义,极难维护
2. 方便 React 过滤内部属性
React 内部有特殊属性如 key 和 ref,组件不应该读取到它们。React 源码在创建 props 对象时,会直接跳过 key 和 ref,确保它们不会出现在你收到的 props 里。
- 对象模式:React 构建新 props 对象时排除
key/ref,简单干净 - 多参数模式:React 怎么知道哪个参数是
key?API 会变得极其混乱
这不是说”因为要过滤 key 所以用对象模式”——对象模式的核心原因是 JSX 语义和组件组合的需要。过滤
key/ref是对象模式带来的便利。
总结
Props 用对象模式的理由:
- JSX 语义:
<Comp a="x" b="y" />天然映射为{ a: "x", b: "y" } - 工程便利性:方便数据透传(
...props) - 框架控制权:方便 React 在中间层过滤内部属性(
key、ref)
不要想得太神圣,它就是 React 定的”快递包装标准”:不管你买的是书还是手机,我都给你装在一个叫
props的纸箱里寄给你。
四、实战:解构 Props
基础写法
function UserCard(props) {
return <div>{props.name}</div>;
}
解构赋值(推荐)
function UserCard({ name, age }: { name: string; age: number }) {
return (
<div>
<p>姓名:{name}</p>
<p>年龄:{age}</p>
</div>
);
}
为什么推荐解构? 三点好处: ① 声明即文档,一眼看出组件需要什么; ② 不用反复写
props.xxx; ③ TypeScript 能精确校验每个字段。
五、Props 黄金法则:单向数据流
Props 是只读的。
- 父组件拥有数据,通过 Props 传给子组件
- 子组件只能读取 Props,不能修改
function UserCard({ name }) {
// ❌ 错误!Props 是只读的,不能直接修改
name = "小黑";
return <div>{name}</div>;
}
为什么?因为 Props 是父组件的数据。如果子组件能随意改,数据流就乱了——你不知道是谁改的,调试会变成噩梦。
如果子组件确实需要”改”数据怎么办?→ 用回调 Props(见第八节)或自己的 State(见 useState完全指南)。
六、传递不同类型的值
Props 能传的不止字符串。记住这条规则:引号传字符串,大括号传万物。
记住:你在 JSX 里写 <Comp name="小白" />,不是在调用函数——你只是在描述”要传什么”,React 来调,调的时候会把属性打包成对象传进去。
| 传什么 | 写法 | 示例 |
|---|---|---|
| 字符串 | 引号 | name="小白" |
| 数字 | 大括号 | age={18} |
| 布尔值 | 大括号 | isAdmin={true} |
| 数组 | 大括号 | tags={['React', 'TS']} |
| 对象 | 大括号 | style={{ color: 'red' }} |
| 函数 | 大括号 | onClick={() => {}} |
| JSX / 组件 | 大括号 | icon={<Star />} |
JSX 属性写法对比
// ✅ 字符串 —— 引号
<GreetCard name="小白" />
// ✅ 数字 —— 大括号(不加大括号会变成字符串 "18")
<AgeCard age={18} />
// ✅ 对象 —— 双层大括号
// 外层 {} 表示"这是 JS 表达式",内层 {} 是对象字面量
<div style={{ color: 'red', fontSize: 16 }} />
// ✅ 布尔值的简写
<Button disabled={true} />
<Button disabled /> {/* 等价于 disabled={true},只适用于布尔 */}
<Button disabled={false} /> {/* 必须显式写 false,不能省略 */}
传递 JSX 作为 Props
你可以把一段 JSX 通过 props 传给子组件,实现灵活的布局组合:
function Layout({ header, content }) {
return (
<div>
<header>{header}</header>
<main>{content}</main>
</div>
);
}
<Layout header={<h1>页面标题</h1>} content={<p>页面内容</p>} />;
七、children:最特殊的 Prop
children 是 React 自动帮你传的 prop——组件标签中间夹的内容。
基础用法
function Card({ children }) {
return <div className="card">{children}</div>;
}
// "你好" 会作为 children 传入
<Card>你好</Card>
// 一段 JSX 也可以
<Card>
<h2>标题</h2>
<p>内容</p>
</Card>
等价于:
// 上面的写法,等价于:
<Card
children={
<>
<h2>标题</h2>
<p>内容</p>
</>
}
/>
// 但没人这么写,标签中间夹内容更直观
为什么 children 重要?
没有 children,你每次想包裹不同内容,都得加一个新的 prop:
// ❌ 没有 children:每种内容都要一个 prop
function Card({ title, body, footer }) {
return (
<div className="card">
<div>{title}</div>
<div>{body}</div>
<div>{footer}</div>
</div>
);
}
// ✅ 用 children:内容随便放,组件只管"壳"
function Card({ children }) {
return <div className="card">{children}</div>;
}
// 用法完全自由
<Card><p>随便什么内容</p></Card>
<Card><UserForm /></Card>
<Card>{someCondition ? <A /> : <B />}</Card>
没有 children,组件是填空题(坑位写死,你填内容);有 children,组件是收纳盒(只管外壳,你随便装)。这是 React 组件组合的核心理念。
TypeScript 中 children 的类型
type CardProps = {
children: React.ReactNode; // 最通用的类型
};
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
React.ReactNode 能接受字符串、数字、JSX、数组、布尔值、null、undefined——基本涵盖了所有可能出现在标签中间的东西。
注意:
boolean虽然是合法的ReactNode,但 React 不会渲染布尔值。<div>{true}</div>和<div>{false}</div>都不会显示任何内容。这个特性常用于条件渲染:<div>{isLoggedIn && <Panel />}</div>。
八、回调 Props:子→父通信
Props 的数据流是单向的(父→子),但子组件经常需要”通知”父组件发生了什么事。怎么做到?把函数作为 Prop 传下去。
基本模式
// 父组件定义函数,通过 prop 传给子组件
function Parent() {
const [name, setName] = useState("小白");
return <Child name={name} onNameChange={setName} />;
// ↑ 函数作为 prop 传下去
}
// 子组件:收到函数,在合适的时机调用它
function Child({ name, onNameChange }) {
return (
<input
value={name}
onChange={e => onNameChange(e.target.value)}
// ↑ 调用父组件传来的函数,把新值"汇报"上去
/>
);
}
数据流:
父组件 ──(name)────→ 子组件 数据向下流
父组件 ←──(新值)──── 子组件 通过回调"汇报"上来
注意:数据仍然是从父组件流到子组件的。子组件没有”直接改”父组件的 state,而是请父组件改——父组件决定要不要改、怎么改。
回调命名约定
| 场景 | 父组件 prop | 子组件调用 |
|---|---|---|
| 值变了 | onChange | onChange(newValue) |
| 点击了 | onClick | onClick() |
| 提交了 | onSubmit | onSubmit(formData) |
| 自定义事件 | onDelete | onDelete(id) |
约定:回调 props 以 on 开头,语义是”当……发生时”。
完整示例:待办列表
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: "学 React", done: false },
]);
// 父组件定义"改"和"删"的逻辑
const toggleTodo = (id: number) => {
setTodos(todos.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(t => t.id !== id));
};
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
);
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
}
要点:TodoItem 完全不知道怎么改数据——它只负责”展示”和”汇报事件”,逻辑全在父组件。这就是单向数据流的好处:数据归谁,逻辑归谁。
九、Props 的默认值
当某个 prop 不是必传的,你需要给一个默认值。
写法一:解构默认值(推荐)
function Button({ size = 'md', color = 'blue' }: ButtonProps) {
// 如果调用方没传 size,就用 'md'
// 如果没传 color,就用 'blue'
return <button className={`btn-${size} btn-${color}`}>Click</button>;
}
<Button /> {/* size='md', color='blue' */}
<Button size="lg" /> {/* size='lg', color='blue' */}
<Button color="red" size="sm" />
写法二:TypeScript 可选属性 + 默认值
type ButtonProps = {
text: string; // 必传
size?: "sm" | "md" | "lg"; // 可选
disabled?: boolean; // 可选
};
function Button({ text, size = "md", disabled = false }: ButtonProps) {
return (
<button className={`btn-${size}`} disabled={disabled}>
{text}
</button>
);
}
规则:? 标记的属性可以不传,解构时给默认值兜底。
写法三:|| 和 ?? 兜底(不推荐作为默认值方案)
function Greet({ name }) {
// ⚠️ 用 || 兜底:name 为 '' 或 0 时也会被替换
return <div>Hello, {name || "陌生人"}</div>;
// ✅ 用 ?? 兜底:只有 null/undefined 才替换
return <div>Hello, {name ?? "陌生人"}</div>;
}
建议:默认值优先用解构默认值(写法一),语义最清晰。
十、Props vs State:别搞混
这是初学者最常混淆的概念,一句话说清:
| Props | State | |
|---|---|---|
| 谁拥有数据 | 父组件 | 当前组件 |
| 能改吗 | ❌ 只读 | ✅ 通过 setter 改 |
| 像什么 | 函数的参数 | 函数的局部变量 |
| 作用 | 从外部接收配置 | 在内部管理变化 |
function Counter({ label }: { label: string }) {
// ↑ label 是 props:父组件传的,只读
const [count, setCount] = useState(0);
// ↑ count 是 state:组件自己的,可以改
return (
<div>
<span>
{label}: {count}
</span>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
<Counter label="点击次数" />;
// ↑ 这是 props,决定组件"展示什么"
// count 是 state,决定组件"记住了什么"
判断口诀:数据从外面来的 → Props;数据自己管的 → State。
更多 State 的细节见 useState完全指南。
十一、TypeScript 类型安全
type ButtonProps = {
text: string;
onClick: () => void;
loading?: boolean;
};
function MyButton({ text, onClick, loading = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={loading}>
{loading ? "加载中..." : text}
</button>
);
}
// ✅ 正确
<MyButton text="提交" onClick={() => alert('点击了')} />
// ❌ 错误:text 必须是字符串
<MyButton text={123} onClick={() => {}} />
类型定义放在哪?
// 推荐:类型定义紧贴组件上方,一目了然
type UserCardProps = {
name: string;
age: number;
avatar?: string;
};
function UserCard({ name, age, avatar }: UserCardProps) { ... }
// 不推荐:用 inline 类型(长了以后可读性差)
function UserCard({ name, age }: { name: string; age: number }) { ... }
用 interface 还是 type?
对于 Props,两者等价。团队选一个统一用就行:
// interface 写法
interface UserCardProps {
name: string;
age: number;
}
// type 写法(本系列统一用 type)
type UserCardProps = {
name: string;
age: number;
};
十二、常见错误
1. 直接修改 Props
// ❌ 直接赋值
function Card({ title }) {
title = title.toUpperCase(); // Props 是只读的
return <h1>{title}</h1>;
}
// ✅ 创建新变量
function Card({ title }) {
const upperTitle = title.toUpperCase();
return <h1>{upperTitle}</h1>;
}
2. 修改 Props 中的对象属性
// ❌ 以为没改 props,其实改了对象内部的属性
function UserCard({ user }) {
user.name = "小黑"; // 修改了 props.user 的属性!
return <div>{user.name}</div>;
}
// ✅ 创建新对象
function UserCard({ user }) {
const displayUser = { ...user, name: "小黑" };
return <div>{displayUser.name}</div>;
}
Props 是只读的,但如果 Props 是对象/数组,你仍然能修改它内部的属性(因为 JS 引用类型的特点)。这违反了 React 的单向数据流原则,会导致难以排查的 bug。
3. 把 Props 当 State 的初始值直接赋值
// ❌ props 变了,state 不会跟着变
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
// 如果父组件传了新的 initialCount,count 不会更新
// 因为 useState 只在首次渲染时使用初始值
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
这个行为是 React 设计的——useState 的初始值只在组件首次挂载时生效。如果需要 props 变化时同步,应该直接用 props 而不是复制到 state:
// ✅ 如果只是展示,直接用 props
function Display({ count }) {
return <span>{count}</span>;
}
4. 忘记传必选 Props
type CardProps = {
title: string; // 必选,没写 ?
};
function Card({ title }: CardProps) {
return <h1>{title}</h1>;
}
// ❌ 编译报错:缺少 title
<Card />;
// TypeScript 会帮你拦住这个错误
这就是为什么 Props 要写 TypeScript 类型——编译器比你的脑子更可靠。
十三、完整实战:可编辑用户卡片
把前面所有知识点串起来:
type UserCardProps = {
user: { name: string; email: string; role: string };
onSave: (user: { name: string; email: string; role: string }) => void;
children?: React.ReactNode;
};
function UserCard({ user, onSave, children }: UserCardProps) {
// 1. State:组件内部管理的编辑状态
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(user);
// 2. 回调 Props:通知父组件保存
const handleSave = () => {
onSave(draft); // 调用父组件传来的函数
setEditing(false);
};
// 3. Props 只读:展示父组件的数据
if (!editing) {
return (
<div className="card">
<h2>{user.name}</h2>
<p>{user.email}</p>
<span>{user.role}</span>
<button onClick={() => setEditing(true)}>编辑</button>
{/* 4. children:容器模式 */}
{children}
</div>
);
}
// 5. 默认值 + 解构:表单编辑
return (
<div className="card">
<input
value={draft.name}
onChange={e => setDraft({ ...draft, name: e.target.value })}
/>
<input
value={draft.email}
onChange={e => setDraft({ ...draft, email: e.target.value })}
/>
<button onClick={handleSave}>保存</button>
<button onClick={() => setEditing(false)}>取消</button>
</div>
);
}
// 使用
function App() {
const [user, setUser] = useState({
name: "小白",
email: "xiaobai@example.com",
role: "developer",
});
return (
<UserCard
user={user} // 传递对象
onSave={updated => setUser(updated)} // 传递回调函数
>
<p className="hint">点击编辑修改信息</p> {/* children */}
</UserCard>
);
}
知识点点拨:
| 行 | 用到的知识点 |
|---|---|
user prop | 对象类型的 Props(第六节) |
onSave prop | 回调 Props,子→父通信(第八节) |
children | 容器模式(第七节) |
useState(editing/draft) | Props vs State 的分工(第十节) |
setDraft({ ...draft, name }) | 不可变更新(对象展开) |
UserCardProps 类型 | TypeScript 类型安全(第十一节) |
总结
| 问题 | 答案 |
|---|---|
| Props 是什么? | 组件函数的参数 |
| 怎么传? | 在 JSX 标签上写属性 <Comp name="value" /> |
| 怎么收? | function Comp(props) 或解构 function Comp({ name }) |
| 核心原则? | 只读,子组件不能修改父组件的 Props |
| 传不同类型? | 引号传字符串,大括号传万物 |
| children 是什么? | 标签中间夹的内容,让组件变成”容器” |
| 子→父通信? | 回调 Props:父传函数,子调函数 |
| 默认值怎么设? | 解构时写 { size = 'md' } |
| Props vs State? | Props = 参数(外面来的),State = 局部变量(自己管的) |
更新时间:2026-05-22
- 进阶:useState完全指南(Props 是只读的,可变数据用 State)
- 相关:Web会话与身份验证
- 相关:React 必备 TS 语法速查(Props 的 TypeScript 类型写法)