Props 属性

组件如果只能写死内容,那复用性就为零——每张"用户卡片"显示的都是同一个名字。Props 就是 React 解决这个问题的方案:让父组件给子组件传数据,就像给 HTML 标签加属性一样自然。

1. Props 是什么

每个组件函数都自动接收一个参数 props,它是一个对象,里面装着父组件传进来的所有数据。

// 组件接收一个 props 对象,里面是父组件传进来的所有属性
function Welcome(props) {
  return <h1>欢迎你,{props.name}!</h1>;
}

// 使用时像 HTML 属性一样写
function App() {
  return (
    <>
      <Welcome name="小明" />
      <Welcome name="小红" />
      <Welcome name="张三" />
    </>
  );
}
// 渲染结果:
// 欢迎你,小明!
// 欢迎你,小红!
// 欢迎你,张三!

<Welcome name="小明" />:React 把它编译成 Welcome({ name: "小明" }) 调用,组件函数收到的 props 就是 { name: "小明" }。这样同一个组件,根据传入的 props 不同,渲染出不同的内容——这就是复用的本质

2. 解构语法:写起来更简洁

实际开发中几乎没人写 props.xxx,而是在参数里直接解构:

// 方式 1:在函数参数里直接解构(最常用)
function Welcome({ name, age }) {
  return (
    <p>
      {name},{age} 岁
    </p>
  );
}

// 方式 2:在函数体里解构(等价,适合 props 字段多时)
function Welcome(props) {
  const { name, age } = props;
  return <p>{name},{age} 岁</p>;
}

// 父组件传值:字符串用引号,数字/布尔/表达式用花括号
function App() {
  return (
    <>
      <Welcome name="小明" age={20} />
      <Welcome name="小红" age={22} />
    </>
  );
}

注意传值时的两种写法:字符串可以用双引号 name="小明",数字、布尔、变量、表达式必须用花括号 age={20}。新手常踩的坑是写 age="20"——这传进去的是字符串,做加法时会变成 "201" 而不是 21

3. 默认值

有时某个 props 不一定要传,这时给它一个默认值,让组件在没传时也能正常工作:

// 方式 1:解构时给默认值(推荐)
function Button({ text = "确定", color = "blue" }) {
  return <button style={{ color }}>{text}</button>;
}

// 没传 text 时用"确定",没传 color 时用"blue"
<Button />                 {/* 蓝色的"确定"按钮 */}
<Button text="取消" />     {/* 蓝色的"取消"按钮 */}
<Button color="red" />     {/* 红色的"确定"按钮 */}

// 方式 2:用 || 短路(默认值为假值时无效,不推荐)
function Tag({ label }) {
  const text = label || "默认";
  return <span>{text}</span>;
}

推荐用解构默认值写法,简洁清晰。注意 || 短路写法在 props 是 0 或空字符串时会用默认值(因为是假值),所以要慎用。

4. props.children:组件的"内容槽"

有些组件是"容器"性质的(卡片、弹窗、布局),它们的内部内容不固定。React 用 props.children 表示组件开闭标签之间的所有内容

// props.children:组件开闭标签之间的所有内容
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="body">
        {children}    {/* 这里会渲染父组件传入的任意 JSX */}
      </div>
    </div>
  );
}

// 用法:在开闭标签之间放任意内容
function App() {
  return (
    <Card title="用户信息">
      <p>姓名:小明</p>
      <p>年龄:20</p>
      <button>编辑</button>
    </Card>
  );
}

children 让组件变得极其灵活——同一个 Card 组件,可以包一段文字,也可以包一个表单,还可以包另一个组件。这是 React"组合"思想的体现,Vue 里叫"插槽(slot)"。

5. 展开运算符传 props

当 props 字段很多,或你有一个现成的对象想整个传进去,用展开运算符 ... :

// 把一个对象的所有属性作为 props 传进去
function App() {
  const user = { name: "小明", age: 20, city: "北京" };
  return <UserCard {...user} />;
  // 等价于:
  // <UserCard name="小明" age={20} city="北京" />
}

// 也可以反过来:在函数签名里把剩余 props 收起来
function Button({ color, ...rest }) {
  // color 单独取出用,其他原样透传给原生 button
  return <button style={{ color }} {...rest} />;
}

这种"透传 props"的模式在封装第三方组件时特别有用:你想给原生 button 加点功能,又不想丢掉它的 onClickdisabled 等原生属性,就用 ...rest 收起来原样传下去。

6. Props 是只读的(铁律)

这是 React 最重要的一条规则:子组件绝不能修改自己收到的 props。所有 props 都是只读的。

// ❌ 错误:props 是只读的,绝对不能在子组件里改
function Counter({ count }) {
  count = count + 1;   // 报错!props 只读
  return <p>{count}</p>;
}

// ✅ 正确:需要变化的数据,用 state(下章讲)
// 父组件管 state,通过 props 把值传给子组件
// 子组件想改,通过"回调函数"通知父组件改
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <Display count={count} />
      <button onClick={() => setCount(count + 1)}>+1</button>
    </>
  );
}

为什么这么严格?因为 props 是父组件"传下来"的,如果子组件能改,那父组件的状态就不可预测了,调试时根本追不清"谁改了谁"。需要变化的数据,用 state(下一章讲)。子组件想"改"父组件的数据,只能通过回调函数:父组件传一个函数下来,子组件调用它,让父组件自己去改。

7. 单向数据流

Props 形成的数据流是单向的:父 → 子 → 孙。数据只能往下传,事件只能往上传(通过回调)。这种"单向"看起来啰嗦,但调试极其清晰——任何一个组件的 props,你顺着父组件往上找一定能找到源头。Angular 早期的"双向绑定"看起来爽,大型项目却维护噩梦,所以现在所有现代框架都学 React 走单向。

8. 数据传递太深怎么办?

如果 props 要传五六层(爷爷 → 爸爸 → 儿子 → 孙子),中间的组件只是"过路",代码很啰嗦——这叫 prop drilling(属性钻取)。React 提供了 Context 来跨层传递,后面有专门一章讲。先记住:简单场景用 props,复杂场景用 Context。

小结

Props 是父传子的数据通道,只读、单向。掌握解构、默认值、children、展开运算符,你就掌握了 props 99% 的用法。下一篇我们看 React 让数据"动起来"的关键——state 与 useState

← 上一篇 React 函数组件

下一篇 State 与 useState

✈️💬