WinddSnow

React-Function-Components-and-Props

字数统计: 3.9k阅读时长: 16 min
2026/07/31

第88课:函数组件与 Props——组件定义、Props 只读、children、组合

React 应用由组件构成。组件是独立的、可复用的 UI 片段,接收输入数据(称为 Props),返回描述界面外观的 React 元素。在 React 的现代版本中,函数组件是定义组件的推荐方式——它简洁、易于测试,并且与 Hooks 深度集成。理解 Props 的只读性单向数据流以及如何通过 children组合模式构建灵活的可复用组件,是掌握 React 组件化开发的核心。


1. 函数组件:最简单的组件形式

函数组件就是一个接收 Props 对象作为参数、返回 React 元素的 JavaScript 函数

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}!</h1>;
}

使用该组件:

1
2
<Welcome name="Alice" />
<Welcome name="Bob" />

1.1 组件的命名规范

React 组件名必须以大写字母开头。这是因为在 JSX 中,小写标签名(如 <div>)被视为 HTML 原生标签,大写标签名(如 <Welcome>)被视为 React 组件。如果组件名以小写开头,React 会将其当作普通 DOM 标签处理,导致渲染失败或产生意外的 HTML 元素。

1
2
3
4
5
// ✅ 正确:大写开头
function MyComponent() { return <div />; }

// ❌ 错误:小写开头,JSX 会将其视为 HTML 标签 <mycomponent>
function myComponent() { return <div />; }

1.2 Props 的解构

在函数组件中,可以通过解构赋值直接提取 Props 中的属性,使代码更简洁:

1
2
3
4
5
6
7
8
9
10
function Welcome({ name, age }) {
return (
<div>
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
</div>
);
}

<Welcome name="Alice" age={25} />

通过解构,参数的意图一目了然,无需在函数体内反复写 props.xxx

1.3 Props 的默认值

可以通过解构赋值的默认值语法为 Props 提供后备值:

1
2
3
4
5
6
7
8
9
10
11
function Welcome({ name = 'Guest', age = 0 }) {
return (
<div>
<h1>Hello, {name}!</h1>
{age > 0 && <p>Age: {age}</p>}
</div>
);
}

<Welcome /> // 渲染 "Hello, Guest!"
<Welcome name="Bob" /> // 渲染 "Hello, Bob!"

这种方式比旧的 Component.defaultProps 静态属性更直观,且 TypeScript 支持更好。


2. Props 的只读性与纯函数原则

React 有一个严格的规则:所有 React 组件都必须像纯函数一样保护它们的 Props 不被修改。这意味着组件内部绝对不能修改自己的 Props——Props 是由父组件传递下来的“只读数据”。

1
2
3
4
5
// ❌ 错误:试图修改 Props
function Welcome(props) {
props.name = 'Hacker'; // 绝不要这样做!
return <h1>Hello, {props.name}!</h1>;
}

2.1 为什么 Props 必须是只读的?

  • 单向数据流:React 的数据从父组件流向子组件,形成一棵数据流动的树。如果子组件可以修改 Props,就会破坏这种单向流动,导致数据变化的来源无法追踪。
  • 可预测性:纯函数(给定相同输入,始终返回相同输出,无副作用)使得组件的渲染结果仅取决于其 Props 和 State,便于调试和测试。
  • 性能优化:React 依赖于 Props 的不可变性来决定是否需要重新渲染组件。如果 Props 被修改,React 的浅比较优化会失效。

2.2 如果需要修改数据怎么办?

当组件需要响应交互改变数据时,应该使用 State(第 89 课详述)。State 是组件内部管理的可变数据,可以通过 useState 等 Hooks 创建。子组件通过回调函数通知父组件修改数据——父组件更新自己的 State,然后将新值通过 Props 重新传递给子组件。这种“数据向下流动,事件向上冒泡”的模式是 React 数据管理的核心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 父组件持有 State
function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} onIncrement={() => setCount(count + 1)} />;
}

// 子组件接收 Props + 回调
function Child({ count, onIncrement }) {
return (
<div>
<p>Count: {count}</p>
<button onClick={onIncrement}>+1</button>
</div>
);
}

3. children:组件的嵌套内容

JSX 标签的嵌套内容会通过一个特殊的 Prop——children——传递给组件。这使得组件可以包裹任意的子内容,实现“容器”型组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Card({ title, children }) {
return (
<div className="card">
<h2 className="card-title">{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}

// 使用
<Card title="通知">
<p>你有一条新消息。</p>
<button>查看</button>
</Card>

children 可以是任何 React 可以渲染的内容:

  • 字符串或数字
  • 单个 React 元素
  • 多个 React 元素(自动作为数组传递)
  • 数组(使用 .map() 生成的列表)
  • nullundefined(表示没有子内容)
  • 函数(Render Props 模式,用于传递状态给子元素)

3.1 children 的类型

在 TypeScript 中,children 的类型通常为 ReactNode,它涵盖了所有可渲染的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ReactNode } from 'react';

interface CardProps {
title: string;
children?: ReactNode; // 可选,允许空内容
}

function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}

也可以使用 PropsWithChildren 工具类型简化:

1
2
3
4
5
6
7
8
9
import { PropsWithChildren } from 'react';

interface CardProps {
title: string;
}

function Card({ title, children }: PropsWithChildren<CardProps>) {
// children 自动获得 ReactNode 类型
}

3.2 多个 children 的占位

有时一个组件需要将 children 分发到不同的位置。可以通过多个具名 Props(而非单个 children)来传递多个子内容区域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Layout({ header, sidebar, children }) {
return (
<div className="layout">
<header className="layout-header">{header}</header>
<aside className="layout-sidebar">{sidebar}</aside>
<main className="layout-main">{children}</main>
</div>
);
}

// 使用
<Layout
header={<h1>Dashboard</h1>}
sidebar={<nav>菜单</nav>}
>
<p>主要内容区域</p>
</Layout>

这种方式被称为槽(Slots)模式,在 React 中通过 Props 而非具名插槽实现。它使得布局组件极其灵活。


4. 组合模式:组件的复用哲学

React 推崇组合而非继承。在面向对象 UI 框架中,通常通过继承基类来复用功能。React 则鼓励通过组合——将组件作为 Props 或 children 传递——来构建复杂的 UI。

4.1 容器组件与展示组件

一种经典的组合模式是将容器组件(负责数据获取和状态管理)和展示组件(负责渲染 UI)分离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 展示组件:只关心渲染,不关心数据来源
function UserList({ users, onUserClick }) {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onUserClick(user)}>
{user.name}
</li>
))}
</ul>
);
}

// 容器组件:负责数据逻辑
function UserListContainer() {
const [users, setUsers] = useState([]);

useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);

return <UserList users={users} onUserClick={u => console.log(u)} />;
}

这种分离使得展示组件高度可复用(可以搭配不同的数据源),容器组件专注于数据逻辑。

4.2 特殊化:通过 Props 定制通用组件

有时一个通用组件需要针对特定场景做变体。与其创建新组件或使用继承,不如通过 Props 控制行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Button({ variant = 'primary', size = 'md', children, ...rest }) {
const baseClass = 'btn';
const variantClass = `btn-${variant}`;
const sizeClass = size !== 'md' ? `btn-${size}` : '';

return (
<button className={`${baseClass} ${variantClass} ${sizeClass}`} {...rest}>
{children}
</button>
);
}

// 通过 Props 定制不同变体
<Button variant="danger" size="sm">删除</Button>
<Button variant="primary">保存</Button>

这样,一个 Button 组件通过 Props 的“配置”就可以产生多种变体,而不是创建 PrimaryButtonDangerButton 等独立组件。


5. 综合示例:博客文章预览卡片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function Card({ title, image, children, footer }) {
return (
<div className="card">
{image && <img src={image} alt={title} className="card-img" />}
<div className="card-body">
<h3 className="card-title">{title}</h3>
<div className="card-content">{children}</div>
</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}

function BlogPost({ post }) {
return (
<Card
title={post.title}
image={post.coverImage}
footer={
<span>
{post.author} · {post.date}
</span>
}
>
<p>{post.excerpt}</p>
<button onClick={() => alert('阅读更多')}>阅读全文</button>
</Card>
);
}

// 使用
<BlogPost
post={{
title: '深入理解 React Props',
excerpt: 'Props 是 React 组件之间传递数据的主要方式……',
coverImage: '/images/react-props.png',
author: '张三',
date: '2026-04-15',
}}
/>

设计要点

  • Card 是通用的容器组件,通过 imagefooter 等 Props 控制各区域显示。
  • BlogPost 是业务组件,组合 Card 并传入具体内容。
  • children 用于传递主体内容(摘要 + 按钮),footer 是具名槽。
  • 所有数据通过 Props 向下流动,组件本身不产生副作用。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 在 React 中,Props 是?
    A. 组件内部的可变状态。
    B. 由父组件传递给子组件的只读数据。
    C. 只能在类组件中使用的配置对象。
    D. 组件返回的 React 元素。

  2. (单选) 以下关于 children 的描述,哪项是正确的?
    A. children 只能传递字符串内容。
    B. children 是 Props 的一个特殊属性,表示 JSX 标签的嵌套内容。
    C. 一个组件只能有一个 children 区域。
    D. children 必须显式声明才能使用。

  3. (填空) React 组件的名称必须以 ______ 开头,以便 JSX 能区分 HTML 原生标签和 React 组件。

  4. (多选) 以下哪些是 React 推荐的可复用组件设计策略?
    A. 通过 Props 的多个槽位(如 headerfooter)传递不同的子内容。
    B. 使用继承基类的方式扩展组件功能。
    C. 通过 Props 控制组件的变体(如 variant)。
    D. 将通用组件特殊化时创建多个相似组件副本。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要实现一个通用的 Modal(模态对话框)组件。要求如下:

  • 接收 isOpen(是否显示)、onClose(关闭回调)、title(标题)、children(主体内容)和 footer(底部操作区,可选)作为 Props。
  • isOpenfalse 时返回 null
  • 渲染一个半透明遮罩层,点击遮罩层时调用 onClose
  • 点击模态框内容区域时阻止事件冒泡(避免点击内容区也关闭模态框)。
  • 使用 PropsWithChildren 类型辅助定义 children
  • 提供一个使用示例:包含一个按钮触发模态框打开,模态框内包含一段文本和“确定”/“取消”两个按钮。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 React 函数组件代码。提示词中需明确指定 Props 接口、事件处理逻辑和组合模式。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个 React 通用 Modal 组件。需要创建以下文件:

  1. src/components/Modal.jsx
    • 使用函数组件,接收 isOpen, onClose, title, children, footer(可选)作为 Props。
    • isOpenfalse,返回 null
    • 渲染一个 div 作为遮罩层(position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;),点击遮罩时调用 onClose
    • 内部渲染一个内容 div(白色背景,圆角,内边距),点击时调用 e.stopPropagation() 阻止关闭。
    • 内容区包含 titleh2)、children 和可选的 footer
    • 使用 PropsWithChildren 定义 Props 类型。
  2. src/App.jsx
    • 使用 useState 管理 isOpen 状态。
    • 渲染一个按钮,点击时打开 Modal。
    • Modal 的 title"确认删除"children<p>确定要删除这条记录吗?此操作不可撤销。</p>footer 包含两个按钮:<button onClick={onClose}>取消</button><button onClick={handleConfirm}>确定</button>
  3. 所有组件添加 JSDoc 注释,确保样式清晰,代码可直接运行。完成后列出所有文件内容。

四、面试真题与参考答案

题目(美团前端面试题):

请解释 React 中 Props 的只读原则,以及它与单向数据流的关系。如果子组件需要修改父组件传递的数据,应该如何正确地实现?请给出一个具体示例。

参考答案

React 中 Props 是只读的——组件内部绝对不能修改自己的 Props。这是 React 单向数据流的核心要求:数据从父组件流向子组件,形成一棵自上而下的流动树。只读 Props 保证了数据流的可预测性——任何给定的 Props 组合总是产生相同的渲染结果(符合纯函数原则),也便于调试(数据来源唯一)和性能优化(Props 不可变使浅比较高效)。

当子组件需要“修改”父组件的数据时,实际的做法不是修改 Props 本身,而是通过回调函数通知父组件。父组件将修改自身 State 的函数作为 Props 传递给子组件,子组件调用该函数并传入新的值,父组件更新 State 后通过新的 Props 将最新数据再次传递给子组件。这种“数据向下,事件向上”的模式是 React 数据管理的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent() {
const [value, setValue] = useState('');
return <Child value={value} onChange={setValue} />;
}

function Child({ value, onChange }) {
return (
<input
value={value}
onChange={e => onChange(e.target.value)}
/>
);
}

这里 Child 没有修改 value Props——它调用 onChange 回调,让 Parent 自己更新状态。这一模式确保了数据流的单向性和可追溯性。


课后练习答案

一、概念自测答案

  1. B

    • 解析:Props 是从父组件传递给子组件的只读数据。A 描述的是 State;C 错误,函数组件也使用 Props;D 错误,Props 是输入而非输出。
  2. B

    • 解析:children 是 Props 的特殊属性,包含 JSX 标签的嵌套内容。A 错误(可传递任何可渲染内容);C 错误(可以有多个具名槽);D 错误(无需显式声明即可通过 props.children 访问)。
  3. 大写字母

    • 解析:React 组件名必须以大写字母开头,以便 JSX 区分原生 HTML 标签和 React 组件。
  4. A、C

    • 解析:A 正确(具名槽模式);C 正确(Props 控制变体)。B 错误,React 推崇组合而非继承;D 错误,应通过 Props 定制变体而非复制组件。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 React 实现一个通用 Modal 组件。要求:

  • Props: isOpen: boolean, onClose: () => void, title: string, children: ReactNode, footer?: ReactNode
  • isOpenfalse 时返回 null
  • 渲染半透明遮罩层,点击遮罩层调用 onClose。内容区点击时 e.stopPropagation() 阻止关闭。
  • 内容区包含 titlechildrenfooter
  • 在 App 中提供使用示例:一个按钮触发 Modal 显示/隐藏。
  • 使用 TypeScript,PropsWithChildren 辅助类型。输出完整组件代码。”
CATALOG
  1. 1. 第88课:函数组件与 Props——组件定义、Props 只读、children、组合
    1. 1.1. 1. 函数组件:最简单的组件形式
      1. 1.1.1. 1.1 组件的命名规范
      2. 1.1.2. 1.2 Props 的解构
      3. 1.1.3. 1.3 Props 的默认值
    2. 1.2. 2. Props 的只读性与纯函数原则
      1. 1.2.1. 2.1 为什么 Props 必须是只读的?
      2. 1.2.2. 2.2 如果需要修改数据怎么办?
    3. 1.3. 3. children:组件的嵌套内容
      1. 1.3.1. 3.1 children 的类型
      2. 1.3.2. 3.2 多个 children 的占位
    4. 1.4. 4. 组合模式:组件的复用哲学
      1. 1.4.1. 4.1 容器组件与展示组件
      2. 1.4.2. 4.2 特殊化:通过 Props 定制通用组件
    5. 1.5. 5. 综合示例:博客文章预览卡片
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)