React 组件通信精讲:用 Props 搭建可复用的 UI 乐高

4 阅读7分钟

React 组件通信精讲:用 Props 搭建可复用的 UI 乐高

在 React 的世界里,组件(Component) 是构建用户界面的基本单元。就像拼搭乐高积木一样,我们可以将一个复杂的页面拆解成多个小而专注的组件,再通过组合、嵌套的方式“拼”出完整的应用。而让这些积木彼此协作的关键,就是 props —— 父组件向子组件传递数据的桥梁。本文将从基础概念出发,逐步深入,带你全面理解 React 中的组件结构、数据来源(state 与 props)、通信方式以及高级用法。

一、组件:开发任务的最小单元

在 React 中,组件是构建 UI 的基本单位。你可以把它理解为一个“功能盒子”:它有自己的结构(JSX)、样式(CSS 或内联样式)和逻辑(JavaScript)。每一个组件都封装了特定的功能或视觉表现,比如一个按钮、一张用户卡片、一个弹窗、甚至整个页面。

1.1 组件的组织方式

通常,我们会将组件文件放在项目中的 components/ 目录下。例如:

src/
├── App.jsx
└── components/
    ├── Greeting.jsx
    ├── Card.jsx
    └── Modal.jsx

这样做的好处是:

  • 职责清晰:每个文件只做一件事;
  • 便于复用:写一次,多处用;
  • 利于协作:不同开发者可以并行开发不同组件,只要约定好接口即可。

1.2 组件的嵌套与层级关系

组件之间可以互相嵌套,形成树状结构。最顶层通常是 App 组件,它就像“老板”,负责协调全局;而其他组件如 GreetingCard 则像“员工”,各司其职。

// App.jsx
function App() {
  return (
    <div>
      <Greeting name="张三" />
      <Card>...</Card>
    </div>
  );
}

在这个例子中,App 是父组件,GreetingCard 是子组件。这种父子关系天然引出了一个问题:数据如何在它们之间流动?

二、State 与 Props:组件数据的两种来源

React 组件要显示内容,必须依赖数据。而这些数据主要有两种来源:组件自己内部产生的(state) ,和由外部传入的(props) 。理解这两者的区别,是掌握 React 数据流的关键。

2.1 State:组件自有、可变的状态

State 是组件内部的状态数据,它代表了组件在某一时刻的“记忆”。比如,一个计数器当前的数值、一个表单是否被提交、一个弹窗是否打开等,都可以用 state 来表示。

在函数组件中,我们通过 useState Hook 来声明和更新 state:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // count 是 state

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

关键点:

  • state 是私有的:只有定义它的组件能读取和修改;
  • state 是可变的:通过 setCount 这样的 setter 函数更新;
  • state 变化会触发重新渲染:React 会自动更新 UI 以反映最新状态。

2.2 Props:外部传入、只读的配置

Props(properties 的缩写)是父组件传递给子组件的数据。你可以把它想象成函数的参数:当调用一个函数时,你传入参数;当使用一个组件时,你通过 JSX 属性传入 props。

例如:

// 父组件
<Greeting name="李四" message="欢迎加入团队!" showIcon={true} />
// 子组件 Greeting.jsx
function Greeting(props) {
  return (
    <div>
      {props.showIcon && <span>👋</span>}
      <h1>Hello, {props.name}!</h1>
      <p>{props.message}</p>
    </div>
  );
}

或者使用解构语法更简洁:

function Greeting({ name, message, showIcon }) {
  // ...
}

关键点:

  • props 是只读的:子组件不能修改 props,只能读取;
  • props 可以是任意类型:字符串、数字、布尔值、对象、数组、函数,甚至其他组件或 JSX;
  • props 是组件复用的基础:同一个 Greeting 组件,传入不同的 name,就能显示不同的欢迎语。

2.3 State 与 Props 的协作关系

在实际开发中,父组件通常持有 state,子组件通过 props 接收数据。这是一种典型的“状态提升”模式。

例如:

// App.jsx(父组件)
function App() {
  const [userName] = useState("王五"); // 状态在这里
  return <Greeting name={userName} />; // 通过 props 传给子组件
}

这样设计的好处是:

  • 状态集中管理:避免状态分散在多个组件中,难以追踪;
  • 子组件更纯粹:只负责展示,不关心数据来源;
  • 便于测试与复用:子组件的行为完全由 props 决定,输入相同,输出就相同。

2.4 单向数据流:React 的核心原则

React 强制采用 单向数据流(Unidirectional Data Flow) :数据只能从父组件流向子组件,不能反向流动。这使得整个应用的数据流向清晰、可预测,极大降低了调试难度。

如果子组件需要“告诉”父组件某些事情(比如用户点击了按钮),它不能直接修改父组件的 state,而是通过 回调函数(作为 props 传递) 通知父组件,由父组件决定是否更新 state。

三、Props 的高级用法:传递组件与 JSX

React 的强大之处在于,props 不仅能传简单数据,还能传函数、JSX 片段,甚至是完整的 React 组件。这为构建高度灵活的通用组件提供了可能。

3.1 children:React 最强大的“插槽”机制

在 React 中,children 是一个特殊且极其重要的 prop。它代表了组件标签之间的所有内容。当你这样使用一个组件时:

<Card>
  <h2>张三</h2>
  <p>高级前端工程师</p>
  <button>查看详情</button>
</Card>

React 会自动将 <h2>...</h2><p>...</p><button>...</button> 这些 JSX 元素收集起来,作为 children prop 传递给 Card 组件。

Card 内部,你可以像使用普通 prop 一样使用它:

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

为什么 children 如此重要?

  1. 实现真正的“组合”而非“配置”
    传统组件可能通过多个 props 传入标题、描述、操作按钮等:

    <Card title="张三" description="高级前端" actionText="查看详情" />
    

    但这种方式限制了灵活性——你无法插入自定义图标、链接或复杂布局。而 children 允许使用者自由决定内部结构,组件只负责提供外层容器和样式。

  2. 天然支持任意嵌套内容
    children 可以是:

    • 纯文本(如 "Hello"
    • 单个 React 元素(如 <p>文本</p>
    • 元素数组(如 [<a/>, <b/>]
    • 甚至其他组件或条件渲染结果(如 {user && <Profile user={user} />}
  3. 是构建通用容器组件的基础
    几乎所有 UI 库中的布局类组件都重度依赖 children,例如:

    • <Modal>{content}</Modal>
    • <Layout><Sidebar />{main}</Layout>
    • <Panel header="标题">{body}</Panel>
  4. 与 Web Components 的 <slot> 思想一致
    它本质上是一种内容分发(Content Distribution) 机制,让组件具备“插槽”能力,极大提升复用性。

💡 最佳实践:当你发现一个组件需要包裹“不确定内容”时,优先考虑使用 children,而不是拆分成多个 props。

3.2 传递组件:Render Props 模式

更进一步,我们可以把整个组件作为 prop 传递

<Modal 
  HeaderComponent={MyHeader}
  FooterComponent={MyFooter}
>
  <p>弹窗主体内容</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>
  );
}

注意:这里 HeaderComponent 是一个函数组件,所以要用 <HeaderComponent />(带括号)来调用它,而不是 {HeaderComponent}(那样只会显示函数定义)。

这种设计让 Modal 完全不关心头部和底部长什么样,只提供“插槽”,由使用者自由定制。这就是 高阶组件复用 的典型实践。

四、Props 类型校验:保障组件契约

为了防止传错 props 导致运行时错误,React 社区推荐使用 类型校验。在 JavaScript 项目中,常用 prop-types 库:

import PropTypes from 'prop-types';

Greeting.propTypes = {
  name: PropTypes.string.isRequired,    // 必填字符串
  message: PropTypes.string,          // 可选字符串
  showIcon: PropTypes.bool            // 布尔值
};

// 设置默认值
Greeting.defaultProps = {
  message: 'Welcome to our team!',
  showIcon: false
};

当传入不符合类型的 props 时,控制台会发出警告(仅在开发环境)。这相当于为组件定义了一份“使用说明书”,大大提升了团队协作的可靠性。

Card.propTypes = {
  children: PropTypes.node, // node 表示任何可渲染的内容
  className: PropTypes.string,
};

其中 PropTypes.node 是专门用于校验 children 的常用类型,它接受:字符串、数字、React 元素、数组、nullundefinedboolean(会被忽略)等所有能被 React 渲染的内容。

(在 TypeScript 项目中,类型校验在编译期完成,更为严格和高效。)

五、总结:组件化思维的核心

  • 组件是乐高积木:小、独立、专注单一功能;
  • state 是组件的“内存” :存储自身状态,可变且私有;
  • props 是组件的“接口” :接收外部输入,只读且灵活;
  • 数据自上而下流动:父组件通过 props 向子组件传递信息;
  • children 和组件 props 让容器组件具备极强的定制能力;
  • 类型校验 是保障组件健壮性的重要手段。

当你能熟练地将复杂 UI 拆解为组件树,并通过清晰的 props 接口连接它们时,你就真正掌握了 React 的精髓——用组合代替继承,用数据驱动视图

🧩 记住:优秀的 React 开发者,不是写更多代码的人,而是写出更少、更通用、更可组合组件的人。