React 学习:父传子的单项数据流——props

前言

最近在学习 React 的 props ,顺手整理了自己的一些学习代码和笔记。props 是 React 组件通信的核心,玩好了它,你写组件就会像搭积木一样顺手。今天这篇文章,就从最基础的用法开始讲起,一步步带你深入,还会结合实际代码例子来说明。顺便聊聊 React 19 前后的小变化、常见的 CSS 写法,以及那个超级好用的 children props。

文章基于我自己的学习项目,代码来自一个简单的 App,包含 Greeting、Card 和 Modal 组件。咱们边看代码边聊,绝对干货满满。

一、Props 是什么?为什么这么重要?(重点对比 state)

在 React 里,组件要正常工作,就离不开"数据"。数据主要分两类:stateprops。这两兄弟长得有点像,但用途和性格完全不一样,搞清楚它们的关系,你写组件时才会心里有底。

用大白话总结:

对比项 state(状态) props(属性)
是谁的? 组件自己的私有数据 父组件传给子组件的数据
能不能改? 可以改(用 setState 或 useState) 只读!子组件不能直接改
改了会怎样? 修改后组件会重新渲染 只有父组件重新传新值,子组件才会更新
生命周期 属于当前组件,组件销毁就没了 像"快递"一样,从父组件送到子组件
典型用途 表单输入值、开关状态、计数器等可变数据 配置信息、显示内容、回调函数等

用生活例子比喻最清楚

想象你家有个儿子(子组件)和老爸(父组件):

  • state 就像儿子自己的零花钱:他自己赚的(或爸给的初始值),想怎么花就怎么花(修改),爸管不着。
  • props 就像爸给儿子的任务和工具:爸说"今天帮我买瓶酱油,顺便带张电影票回来",儿子只能按爸给的钱和要求去办,不能自己改任务("我偏要买可乐"不行)。

如果儿子想跟爸说"我买完了"或者"我需要更多钱",那只能通过爸事先给的"传话方式"(回调函数)来沟通------这就引出了我们上次讲的"子传父"。

代码对比最直观

来看同一个需求:显示一个问候语,可以改名字和欢迎词。

用 state 的方式(数据在组件自己手里):

jsx 复制代码
import { useState } from 'react';

function GreetingSelf() {
  const [name, setName] = useState('陌生人');
  const [message, setMessage] = useState('欢迎光临');

  return (
    <div>
      <h1>Hello {name}</h1>
      <p>{message}</p>
      
      {/* 自己改自己,超级自由 */}
      <input 
        placeholder="改名字" 
        onChange={(e) => setName(e.target.value)} 
      />
      <input 
        placeholder="改欢迎词" 
        onChange={(e) => setMessage(e.target.value)} 
      />
    </div>
  );
}

这个组件完全自给自足,想改啥改啥。

用 props 的方式(数据从外面传进来):

jsx 复制代码
function Greeting({ name, message }) {
  return (
    <div>
      <h1>Hello {name}</h1>
      <p>{message}</p>
      
      {/* 这里不能改!想改得让父组件来 */}
      {/* <input onChange=... />  // 没用,props 是只读的 */}
    </div>
  );
}

// 父组件
function App() {
  const [name, setName] = useState('keji');
  const [message, setMessage] = useState('欢迎来到腾讯');

  return (
    <div>
      <Greeting name={name} message={message} />
      
      {/* 只有这里能改,改了会重新传给子组件 */}
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={message} onChange={(e) => setMessage(e.target.value)} />
    </div>
  );
}

看到区别了吗?

  • 用 state:组件内部自己管数据,适合独立的小部件。
  • 用 props:组件变成"纯展示"工具,数据统一由父组件管理,适合复用(同一个 Greeting 可以给不同人用不同欢迎词)。

二、React 19 之前:Props 的经典玩法

在 React 18 及更早版本,props 用法已经很成熟了:

  1. 解构 props:直接在参数里解构,用起来最爽。

  2. 默认值:两种方式

    • ES6 默认参数:message = "默认值"
    • defaultProps(静态属性,已被废弃)

    示例(旧方式):

    jsx 复制代码
    Greeting.defaultProps = {
      message: "欢迎来到字节"
    };
  3. 类型检查:用 prop-types 包

    jsx 复制代码
    import PropTypes from "prop-types";
    
    Greeting.propTypes = {
      name: PropTypes.string.isRequired,
      message: PropTypes.string,
    };

    开发模式下,如果传错类型会警告,很实用。

  4. 传任意数据:字符串、数字、对象、函数、甚至 JSX 都可以。

三、React 19 之后:Props 的小变化(更简洁了)

React 19(2024 年 12 月稳定发布)对 props 本身的核心机制没大改,但有一些清理和改进,让代码更现代:

  • 移除 propTypes 和 defaultProps:这些早在 React 15.5 就被标记废弃了,React 19 直接从 React 包里删掉。以后用的话不会报错,但完全没效果。

    • 推荐:用 TypeScript 做静态类型检查,或者继续用独立的 prop-types 包(手动检查)。
    • 默认值统一用 ES6 默认参数,更简洁。
  • ref 现在可以直接作为 prop 传(超级实用!) 以前,函数组件想接收 ref 必须裹一层 forwardRef:

    jsx 复制代码
    // React 18
    const MyInput = React.forwardRef((props, ref) => (
      <input ref={ref} {...props} />
    ));

    React 19 后,函数组件直接收 ref,像 children 一样自然:

    jsx 复制代码
    // React 19
    function MyInput({ ref, ...props }) {
      return <input ref={ref} {...props} />;
    }

    少了好多样板代码,官方还提供了 codemod 自动迁移。

其他 props 相关的小优化:对 Custom Elements(Web Components)的支持更好,props 会优先作为 property 传,而不是 attribute。

总之,React 19 的 props 更干净、更贴近现代 JS。如果你还在用老项目,建议升级时跑一下官方 codemod。

四、React Props 的各种"边界情况"详解:不传、传了不用、没给值会发生什么?

在实际写代码时,props 的这些"边缘情况"经常让人摸不着头脑。今天我们就一个个拆开看,结合真实代码和控制台输出,告诉你到底会发生什么,以及怎么避免坑。

1. 不传某个 prop(完全没写)

情况:父组件使用子组件时,压根没写这个 prop。

结果 :子组件收到这个 prop 的值是 undefined

例子

子组件 Greeting.jsx:

jsx 复制代码
function Greeting({ name, message, showIcon }) {
  console.log({ name, message, showIcon });  // 关键看这里

  return (
    <div>
      <h1>Hello {name}</h1>
      <p>{message}</p>
      {showIcon && <span>👋</span>}
    </div>
  );
}

父组件这样用:

jsx 复制代码
<Greeting />

控制台输出:

JavaScript 复制代码
{ name: undefined, message: undefined, showIcon: undefined }

页面显示:

text 复制代码
Hello          // 空
               // 空

结论:不传 = undefined,不会报错,但显示会很丑。

2. 传了 prop 但子组件没用(写了但没解构/使用)

情况:父组件传了值,但子组件参数里没接收它。

结果:传的值会丢失,子组件根本拿不到。

例子

父组件:

jsx 复制代码
<Greeting name="小明" message="欢迎" />

子组件故意不接收 name:

jsx 复制代码
function Greeting({ message }) {  // 只解构了 message,name 被忽略
  console.log({ message });

  return <h1>Hello {message}</h1>;
}

控制台输出:

JavaScript 复制代码
{ message: "欢迎" }

name 的值"小明"直接丢了,子组件完全不知道有这个 prop。

另一种写法(用 props 对象):

jsx 复制代码
function Greeting(props) {
  console.log(props);  // { name: "小明", message: "欢迎" }

  return <h1>Hello {props.message}</h1>;  // 只用了 message
}

这种情况下 props 对象里是有 name 的,但如果你没用它,就白传了。

结论:传了但没用 = 白传,浪费性能(轻微),代码易混淆。

3. 传了 prop 但没给值()

情况:写了 prop 名,但没赋值(布尔类型常见)。

结果 :React 会自动把这个 prop 的值设为 true

例子

父组件:

jsx 复制代码
<Greeting showIcon />        // 没写 =true
<Greeting showIcon={true} /> // 显式写 true
<Greeting showIcon={false} />// 显式写 false

子组件:

jsx 复制代码
function Greeting({ showIcon }) {
  console.log({ showIcon });

  return showIcon && <span>👋</span>;
}

三种写法的控制台输出:

JavaScript 复制代码
{ showIcon: true }     // <Greeting showIcon />
{ showIcon: true }     // <Greeting showIcon={true} />
{ showIcon: false }    // <Greeting showIcon={false} />

结论:在 JSX 中, 等价于 。这是 React 的语法糖,超级常见于开关类 prop(如 disabled、checked、selected 等)。

4. 给了默认值的情况(ES6 默认参数)

情况:不传或传 undefined 时,用默认值兜底。

例子

jsx 复制代码
function Greeting({ 
  name = "陌生人", 
  message = "欢迎光临", 
  showIcon = false 
}) {
  console.log({ name, message, showIcon });

  return (
    <div>
      <h1>Hello {name}</h1>
      <p>{message}</p>
      {showIcon && <span>👋</span>}
    </div>
  );
}

测试几种调用:

jsx 复制代码
<Greeting />                          // 全不传
<Greeting name={undefined} />         // 显式传 undefined
<Greeting name="" />                  // 传空字符串(不会用默认)
<Greeting name="小明" />

控制台输出:

JavaScript 复制代码
{ name: "陌生人", message: "欢迎光临", showIcon: false }
{ name: "陌生人", message: "欢迎光临", showIcon: false }  // undefined 触发默认
{ name: "",       message: "欢迎光临", showIcon: false }  // 空字符串不会触发默认
{ name: "小明",   message: "欢迎光临", showIcon: false }

关键点 :只有 undefined 才会触发 ES6 默认值。传 null、""、false、0 都不会触发默认值。

5. 总结表格(一目了然)

父组件写法 子组件收到值 是否触发默认值 说明
不写 prop undefined 最常见坑
< Comp flag /> true - 布尔 prop 的语法糖
< Comp flag={true/false} /> true / false - 显式控制
< Comp name={undefined} /> undefined 显式传 undefined 也会触发默认
< Comp name="" /> ""(空字符串) 传了具体值,就不会用默认
< Comp name={null} /> null null 是合法值,不会触发默认
传了 prop 但子组件没接收 拿不到(丢失) - 代码 bug,建议用 TS 或 PropTypes 检查

五、children:最强大的 props

children 是 React 自动传给你的一个特殊 prop,代表组件标签之间的内容。它让组件超级灵活,能"插槽"任意 JSX。

Card 组件

jsx 复制代码
App.jsx
<Card className="user-card">
  <h2>张三</h2>
  <p>高级前端工程师</p>
  <button>查看详情</button>
</Card>

Card 实现

jsx 复制代码
//Card.jsx
const Card = ({ children, className = "" }) => {
  return <div className={`card ${className}`}>{children}</div>;
};

其中,{children}就是

jsx 复制代码
  <h2>张三</h2>
  <p>高级前端工程师</p>
  <button>查看详情</button>

直接把父组件传的 JSX 原样渲染出来,完美封装卡片样式。

Modal 组件(更高级的定制): 我们想让 Modal 支持自定义 header 和 footer:

jsx 复制代码
//App.jsx
<Modal HeaderComponent={MyHeader} FooterComponent={MyFooter}>
  <p>这是一个弹窗</p>
  <p>你可以在这里显示任何JSX</p>
</Modal>

Modal 实现:

jsx 复制代码
//Modal.jsx
function Modal({ HeaderComponent, FooterComponent, children }) {
  return (
    <div style={styles.overlay}>
      <div style={styles.modal}>
        <HeaderComponent />
        <div style={styles.content}>{children}</div>
        <FooterComponent />
      </div>
    </div>
  );
}

这里 children 就是弹窗正文内容,而 Header/Footer 是通过 props 传组件,实现高度定制。

children 的威力在于:它让你的组件像"容器"一样,能包裹任意内容。Ant Design、Material UI 的很多组件都靠它实现灵活性。

六、React 中常见的 CSS 样式写法

React 是"CSS in JS"的鼻祖,样式写法灵活多变。来看几种常见方式,各有优缺点:

  1. 内联样式(style 对象) ------ 最简单,直接在 JSX 里写

    jsx 复制代码
    <h2 style={{ margin: 0, color: "blue" }}>自定义标题</h2>

    适合快速原型或动态样式(比如根据 props 变色)。缺点:没法用伪类(如 :hover),写多了很乱。

  2. 单独 CSS 文件 + className ------ 传统方式,最常见 看 Card.jsx 和 Card.css:

    jsx 复制代码
    import "./Card.css";
    
    const Card = ({ children, className = "" }) => {
      return <div className={`card ${className}`}>{children}</div>;
    };

    CSS 文件里写:

    CSS 复制代码
    .card {
      background-color: #ffffff;
      border-radius: 12px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      /* ... */
    }
    
    .card:hover {
      transform: translateY(-4px);
      /* 悬停效果 */
    }

    优点:支持所有 CSS 特性(伪类、媒体查询),易维护。缺点:类名冲突风险(可以用 CSS Modules 解决)。

  3. CSS in JS(对象样式)

    jsx 复制代码
    const styles = {
      overlay: { backgroundColor: "rgba(0,0,0,0.5)", /* ... */ },
      modal: { backgroundColor: "white", /* ... */ },
    };
    
    <div style={styles.overlay}>...</div>

    优点:样式作用域天然隔离,动态样式超方便。缺点:运行时开销稍大,伪类支持差(需要额外库如 styled-components)。

  4. 其他流行方案

    • styled-componentsemotion:写模板字符串,兼顾作用域和伪类。
    • Tailwind CSS:utility-first,直接在 className 里堆类。
    • CSS Modules:自动生成唯一类名,避免冲突。

个人建议:小项目内联或单独 CSS 够用,中大型项目推荐 CSS Modules + Tailwind,或者 styled-components。

最后总结

props 是 React 的灵魂,掌握它你就掌握了组件通信。默认值、类型检查、children 这些细节用好了,代码会优雅很多。React 19 把一些老古董清理掉,鼓励我们写更现代的代码(ES6 默认 + TS)。

如果你也在学 React,建议自己动手搭几个组件玩玩 props 和 children,感觉完全不一样。

相关推荐
这个需求建议不做2 小时前
pdf.js(pdfdist)踩坑workerSrc报错pdf.worker.mjs无法正确获取
开发语言·javascript·pdf
soda_yo2 小时前
React哲学:保持组件纯粹 哈气就要哈得纯粹
前端·react.js·设计
Bigger2 小时前
Tauri (22)——让 `Esc` 快捷键一层层退出的分层关闭方案
前端·react.js·app
大猫会长2 小时前
react中用css加载背景图的2种情况
开发语言·前端·javascript
子春一22 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的无障碍应用
javascript·flutter·microsoft
专业IT有讠果2 小时前
[Docker/K8S] Kubernetes故障克星:19个高频问题速查与秒解指南(2025版)
javascript·面试
编程修仙2 小时前
第一篇 VUE3的介绍以及搭建自己的VUE项目
前端·javascript·vue.js
web Rookie2 小时前
前端开发中常见的图片格式及使用场景
javascript·css3
星月心城2 小时前
八股文-JavaScript(第一天)
开发语言·前端·javascript