React 组件通信核心精讲:从 Props 到 Children 的优雅实战

54 阅读6分钟

React 组件通信核心精讲:从 Props 到 Children 的优雅实战

在 React 开发中,组件就像一堆精巧的乐高积木,我们通过不断组合、嵌套和复用它们,来搭建出复杂而美观的页面。而要让这些“积木”真正协同工作,核心就在于组件之间的通信。其中,最基础、最常用也最优雅的方式,就是 PropsChildren

今天,我们基于一个真实的小项目(包含 Greeting、Modal、Card 三个自定义组件),来深入剖析 React 组件通信的底层逻辑。

7d51c79f4346e65c2eafb673286d21c7.jpg

一、State vs Props:谁持有数据?谁负责传递?

这是所有 React 新手必须先搞清楚的核心概念。

  • State:组件自有的数据,通常通过 useStateuseReducer 管理。当 state 改变时,组件会重新渲染。它属于组件“内部”,外部无法直接修改(只能通过回调函数间接影响)。
  • Props:父组件向下传递给子组件的数据。子组件只能读取,不能直接修改(单向数据流是 React 的设计哲学)。

形象比喻:
父组件是“老板”,持有资源(state)和决策权;
子组件是“员工”,接受老板指派的任务和参数(props),认真执行,但无权改老板的决定。

// 父组件 App.jsx(老板)
function App() {
  const [userName, setUserName] = useState("陈炳南");
  return <Greeting name={userName} message="欢迎加入腾讯!" />;
}
// 子组件 Greeting.jsx(员工)
function Greeting(props) {
  const { name, message } = props;
  return <h1>Hello, {name}! {message}</h1>;
}

易错提醒
很多人误以为 props 可以像 state 一样直接修改:

props.name = "新名字"; // ❌ 绝对禁止!props 是只读的

这样做不仅不会生效,还会在严格模式下抛出警告。正确做法是:子组件需要“改变”数据时,通过父组件传入的回调函数通知父组件去修改 state。

二、Props 的三种常见传递方式

1. 基本值传递(字符串、数字、布尔)

最简单直接:

<Greeting 
  name="柯基" 
  message={123} 
  showIcon={true}   // 或简写 showIcon (默认值为true)
/>

在子组件中可以通过解构获取,并设置默认值与类型检查:

function Greeting({ name, message = "欢迎来到腾讯", showIcon = false }) {
  return (
    <div>
      {showIcon && <span>👋</span>}
      <h1>Hello, {name}!</h1>
      <p>{message}</p>
    </div>
  );
}

// PropTypes 类型检查(生产环境建议用 TypeScript 替代)
Greeting.propTypes = {
  name: PropTypes.string.isRequired,
  message: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  showIcon: PropTypes.bool,
};

// defaultProps 设置默认值
Greeting.defaultProps = {
  message: "欢迎来到腾讯",
};

优化建议
在现代 React(17+)中,推荐直接在解构参数中设置默认值,而不是使用 Greeting.defaultProps,代码更简洁,也更符合函数式编程风格。

注意:React19中函数组件的 defaultProps以及PropTypes 的运行时检查 被彻底删了,直接忽略,不会生效

2. 传递组件(Render Props 模式的前身)

有时候我们希望子组件的某一部分完全由父组件决定。这时可以把组件本身作为 props 传递

看项目中的 Modal 组件:

<Modal
  HeaderComponent={MyHeader}
  FooterComponent={MyFooter}
>
  <p>这是一个弹窗</p>
  <p>你可以在这显示任何JSX</p>
</Modal>

子组件 Modal 这样渲染:

function Modal({ HeaderComponent, FooterComponent, children }) {
  return (
    <div style={styles.overlay}>
      <div style={styles.modal}>
        <HeaderComponent />   {/* 直接调用传入的组件 */}
        <div style={styles.content}>{children}</div>
        <FooterComponent />
      </div>
    </div>
  );
}

这就是经典的 “组件插槽” 思想,极大提升了组件的定制能力。

底层逻辑
React 中一切皆可作为 props 传递,包括函数、组件、JSX 元素。因为组件本质上就是一个函数!

3. Children Props:最强大、最优雅的插槽机制

React 为我们内置了一个特殊的 prop —— children。它代表组件标签之间的所有内容。

Card 组件就是典型例子:

<Card className="user-card">
  <h2>张三</h2>
  <p>高级前端工程师</p>
  <button>查看详情</button>
</Card>

Card 组件实现非常简洁:

const Card = ({ children, className = '' }) => {
  return (
    <div className={`card ${className}`}>
      {children}
    </div>
  );
};

为什么 children 如此强大?

  • 它可以是文本、元素、组件、甚至多个元素组成的数组。
  • 父组件完全掌控内部结构,子组件只负责“外壳”和样式。
  • 天然支持嵌套,符合组合式设计理念。

易错提醒
很多人会忘记传 children,或者误以为 children 是字符串。其实它是 ReactElement 类型。如果你想在子组件中对 children 进行遍历或改造(比如添加 key、克隆元素注入 props),可以使用 React.Children 工具:

import { Children, cloneElement } from 'react';

Children.map(children, (child) => 
  cloneElement(child, { extraProp: 'value' })
);

三、几个细节知识点

fc962ce0cd306c49bc54248e80437e81.jpg

1.模板字符串(template literal) + 字符串插值

新手看到这个时候 <div className={card ${className}}>

难免一脸懵,这玩意儿啥意思?父组件不是已经传值了吗?

这个是 模板字符串(template literal) + 字符串插值 的经典用法,用来动态拼接 className。

jsx

<div className={`card ${className}`}>
  {children}
</div>

意思是:这个 div 永远会有一个基础类名 card(来自 Card.css 的样式),同时再把父组件传进来的 className 也加进去。

举例来说明:

父组件这样用:

jsx

<Card className="user-card">
  <h2>张三</h2>
  ...
</Card>

渲染出来的 HTML 就是:

HTML

<div class="card user-card">
  <h2>张三</h2>
  ...
</div>

这样做的好处:

  • Card 组件自己保证基础样式(圆角、阴影、padding 等)
  • 父组件可以额外加自己的样式覆盖或扩展(比如不同的背景、边框、宽度等)
  • 非常常见的设计模式,几乎所有 UI 库的组件(Ant Design、Material UI、Chakra UI)都是这么干的

如果父组件不传 className(或者传空字符串),就只会是 card 一个类:

jsx

<Card>
  ...
</Card>
<!-- 渲染成 -->
<div class="card">
  ...
</div>

默认值 className = '' 就是为了防止 undefined 拼接出 card undefined 这种奇怪的 class。

总结:父组件传的值没有被丢掉,而是和子组件自己的基础类名合并了,超级灵活!

设计意图:子组件提供默认好看的样式,父组件随时可以覆盖/扩展,完美平衡“统一性”和“灵活性”。

2.函数表达式 const Card = ({children, className=''})=>{},和函数声明function Greeting(props){} 有啥区别?

两种写法本质上几乎一样,都是定义函数组件,但有细微区别:

jsx

// 方式1:函数声明(Greeting 用这个)
function Greeting(props) {
  ...
}

// 方式2:函数表达式(Card 用这个)
const Card = ({ children, className = '' }) => {
  ...
};

区别对比:

项目函数声明(function)函数表达式(const + =>)
提升(hoisting)会整个函数提升,可以在定义前调用只会变量提升(提升后是 undefined),定义前调用会报错
命名函数名就是组件名,报错栈里好看匿名箭头函数时,报错栈显示 anonymous(不友好)
this 绑定有自己的 this无 this,继承外部(箭头函数特性)
现代项目偏好老派,逐渐减少新派,几乎所有新代码都用这个
默认值写法解构麻烦点超级方便,直接在参数里写默认值

实际影响:

  • 在 React 里,组件渲染顺序是严格从上到下的,几乎不会在定义前使用组件,所以 hoisting 影响不大
  • 最关键的是:箭头函数写法更简洁,支持直接在参数解构默认值,社区 90% 新代码都用这个

建议:新组件统一用箭头函数表达式(const Component = () => {}),更现代、更一致。

四、组件通信的进阶思考

掌握了 props 和 children 后,你已经能应对 80% 的场景。但还有一些值得了解的模式:

  1. 回调函数传参:子 → 父通信的标准方式
  2. Context:跨多层级的全局数据传递(避免 props drilling)
  3. Render Props高阶组件(HOC):更高级的复用逻辑方式
  4. 自定义 Hooks:提取状态逻辑的最佳实践

这些我们可以在后续文章中深入展开。

五、总结:优秀组件设计的三大原则

  1. 单一职责:一个组件只做好一件事
  2. 高内聚低耦合:内部逻辑完整,对外依赖清晰(通过 props 明确声明)
  3. 最大化可组合性:优先使用 children 和函数式 props,让使用者拥有最大自由度

当你开始用“插槽”思维设计组件时,你就真正迈入了 React 中高级开发者的行列。