跳至正文
来两杯美式
返回

React 核心(3):Props 完全指南

By 来两杯美式
发布于更新于

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”?

PropsProperties(属性)的缩写,可以理解为组件的”配置单”。

类似 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 内部有特殊属性如 keyref,组件不应该读取到它们。React 源码在创建 props 对象时,会直接跳过 keyref,确保它们不会出现在你收到的 props 里。

这不是说”因为要过滤 key 所以用对象模式”——对象模式的核心原因是 JSX 语义和组件组合的需要。过滤 key/ref 是对象模式带来的便利

总结

Props 用对象模式的理由:

  1. JSX 语义<Comp a="x" b="y" /> 天然映射为 { a: "x", b: "y" }
  2. 工程便利性:方便数据透传(...props
  3. 框架控制权:方便 React 在中间层过滤内部属性(keyref

不要想得太神圣,它就是 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 是只读的。

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、数组、布尔值、nullundefined——基本涵盖了所有可能出现在标签中间的东西。

注意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子组件调用
值变了onChangeonChange(newValue)
点击了onClickonClick()
提交了onSubmitonSubmit(formData)
自定义事件onDeleteonDelete(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:别搞混

这是初学者最常混淆的概念,一句话说清:

PropsState
谁拥有数据父组件当前组件
能改吗❌ 只读✅ 通过 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


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 3 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(4):useState 完全指南
下一篇
React 核心(2):声明式与 React 的实现机制