第88课:函数组件与 Props——组件定义、Props 只读、children、组合
React 应用由组件构成。组件是独立的、可复用的 UI 片段,接收输入数据(称为 Props),返回描述界面外观的 React 元素。在 React 的现代版本中,函数组件是定义组件的推荐方式——它简洁、易于测试,并且与 Hooks 深度集成。理解 Props 的只读性、单向数据流以及如何通过 children 和组合模式构建灵活的可复用组件,是掌握 React 组件化开发的核心。
1. 函数组件:最简单的组件形式
函数组件就是一个接收 Props 对象作为参数、返回 React 元素的 JavaScript 函数。
1 | function Welcome(props) { |
使用该组件:
1 | <Welcome name="Alice" /> |
1.1 组件的命名规范
React 组件名必须以大写字母开头。这是因为在 JSX 中,小写标签名(如 <div>)被视为 HTML 原生标签,大写标签名(如 <Welcome>)被视为 React 组件。如果组件名以小写开头,React 会将其当作普通 DOM 标签处理,导致渲染失败或产生意外的 HTML 元素。
1 | // ✅ 正确:大写开头 |
1.2 Props 的解构
在函数组件中,可以通过解构赋值直接提取 Props 中的属性,使代码更简洁:
1 | function Welcome({ name, age }) { |
通过解构,参数的意图一目了然,无需在函数体内反复写 props.xxx。
1.3 Props 的默认值
可以通过解构赋值的默认值语法为 Props 提供后备值:
1 | function Welcome({ name = 'Guest', age = 0 }) { |
这种方式比旧的 Component.defaultProps 静态属性更直观,且 TypeScript 支持更好。
2. Props 的只读性与纯函数原则
React 有一个严格的规则:所有 React 组件都必须像纯函数一样保护它们的 Props 不被修改。这意味着组件内部绝对不能修改自己的 Props——Props 是由父组件传递下来的“只读数据”。
1 | // ❌ 错误:试图修改 Props |
2.1 为什么 Props 必须是只读的?
- 单向数据流:React 的数据从父组件流向子组件,形成一棵数据流动的树。如果子组件可以修改 Props,就会破坏这种单向流动,导致数据变化的来源无法追踪。
- 可预测性:纯函数(给定相同输入,始终返回相同输出,无副作用)使得组件的渲染结果仅取决于其 Props 和 State,便于调试和测试。
- 性能优化:React 依赖于 Props 的不可变性来决定是否需要重新渲染组件。如果 Props 被修改,React 的浅比较优化会失效。
2.2 如果需要修改数据怎么办?
当组件需要响应交互改变数据时,应该使用 State(第 89 课详述)。State 是组件内部管理的可变数据,可以通过 useState 等 Hooks 创建。子组件通过回调函数通知父组件修改数据——父组件更新自己的 State,然后将新值通过 Props 重新传递给子组件。这种“数据向下流动,事件向上冒泡”的模式是 React 数据管理的核心。
1 | // 父组件持有 State |
3. children:组件的嵌套内容
JSX 标签的嵌套内容会通过一个特殊的 Prop——children——传递给组件。这使得组件可以包裹任意的子内容,实现“容器”型组件。
1 | function Card({ title, children }) { |
children 可以是任何 React 可以渲染的内容:
- 字符串或数字
- 单个 React 元素
- 多个 React 元素(自动作为数组传递)
- 数组(使用
.map()生成的列表) null或undefined(表示没有子内容)- 函数(Render Props 模式,用于传递状态给子元素)
3.1 children 的类型
在 TypeScript 中,children 的类型通常为 ReactNode,它涵盖了所有可渲染的类型:
1 | import { ReactNode } from 'react'; |
也可以使用 PropsWithChildren 工具类型简化:
1 | import { PropsWithChildren } from 'react'; |
3.2 多个 children 的占位
有时一个组件需要将 children 分发到不同的位置。可以通过多个具名 Props(而非单个 children)来传递多个子内容区域:
1 | function Layout({ header, sidebar, children }) { |
这种方式被称为槽(Slots)模式,在 React 中通过 Props 而非具名插槽实现。它使得布局组件极其灵活。
4. 组合模式:组件的复用哲学
React 推崇组合而非继承。在面向对象 UI 框架中,通常通过继承基类来复用功能。React 则鼓励通过组合——将组件作为 Props 或 children 传递——来构建复杂的 UI。
4.1 容器组件与展示组件
一种经典的组合模式是将容器组件(负责数据获取和状态管理)和展示组件(负责渲染 UI)分离:
1 | // 展示组件:只关心渲染,不关心数据来源 |
这种分离使得展示组件高度可复用(可以搭配不同的数据源),容器组件专注于数据逻辑。
4.2 特殊化:通过 Props 定制通用组件
有时一个通用组件需要针对特定场景做变体。与其创建新组件或使用继承,不如通过 Props 控制行为:
1 | function Button({ variant = 'primary', size = 'md', children, ...rest }) { |
这样,一个 Button 组件通过 Props 的“配置”就可以产生多种变体,而不是创建 PrimaryButton、DangerButton 等独立组件。
5. 综合示例:博客文章预览卡片
1 | function Card({ title, image, children, footer }) { |
设计要点:
Card是通用的容器组件,通过image、footer等 Props 控制各区域显示。BlogPost是业务组件,组合Card并传入具体内容。children用于传递主体内容(摘要 + 按钮),footer是具名槽。- 所有数据通过 Props 向下流动,组件本身不产生副作用。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 在 React 中,Props 是?
A. 组件内部的可变状态。
B. 由父组件传递给子组件的只读数据。
C. 只能在类组件中使用的配置对象。
D. 组件返回的 React 元素。(单选) 以下关于
children的描述,哪项是正确的?
A.children只能传递字符串内容。
B.children是 Props 的一个特殊属性,表示 JSX 标签的嵌套内容。
C. 一个组件只能有一个children区域。
D.children必须显式声明才能使用。(填空) React 组件的名称必须以
______开头,以便 JSX 能区分 HTML 原生标签和 React 组件。(多选) 以下哪些是 React 推荐的可复用组件设计策略?
A. 通过 Props 的多个槽位(如header、footer)传递不同的子内容。
B. 使用继承基类的方式扩展组件功能。
C. 通过 Props 控制组件的变体(如variant)。
D. 将通用组件特殊化时创建多个相似组件副本。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要实现一个通用的 Modal(模态对话框)组件。要求如下:
- 接收
isOpen(是否显示)、onClose(关闭回调)、title(标题)、children(主体内容)和footer(底部操作区,可选)作为 Props。 - 当
isOpen为false时返回null。 - 渲染一个半透明遮罩层,点击遮罩层时调用
onClose。 - 点击模态框内容区域时阻止事件冒泡(避免点击内容区也关闭模态框)。
- 使用
PropsWithChildren类型辅助定义children。 - 提供一个使用示例:包含一个按钮触发模态框打开,模态框内包含一段文本和“确定”/“取消”两个按钮。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 React 函数组件代码。提示词中需明确指定 Props 接口、事件处理逻辑和组合模式。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建一个 React 通用 Modal 组件。需要创建以下文件:
src/components/Modal.jsx:
- 使用函数组件,接收
isOpen,onClose,title,children,footer(可选)作为 Props。- 若
isOpen为false,返回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()阻止关闭。- 内容区包含
title(h2)、children和可选的footer。- 使用
PropsWithChildren定义 Props 类型。src/App.jsx:
- 使用
useState管理isOpen状态。- 渲染一个按钮,点击时打开 Modal。
- Modal 的
title为"确认删除",children为<p>确定要删除这条记录吗?此操作不可撤销。</p>,footer包含两个按钮:<button onClick={onClose}>取消</button>和<button onClick={handleConfirm}>确定</button>。- 所有组件添加 JSDoc 注释,确保样式清晰,代码可直接运行。完成后列出所有文件内容。
四、面试真题与参考答案
题目(美团前端面试题):
请解释 React 中 Props 的只读原则,以及它与单向数据流的关系。如果子组件需要修改父组件传递的数据,应该如何正确地实现?请给出一个具体示例。
参考答案:
React 中 Props 是只读的——组件内部绝对不能修改自己的 Props。这是 React 单向数据流的核心要求:数据从父组件流向子组件,形成一棵自上而下的流动树。只读 Props 保证了数据流的可预测性——任何给定的 Props 组合总是产生相同的渲染结果(符合纯函数原则),也便于调试(数据来源唯一)和性能优化(Props 不可变使浅比较高效)。
当子组件需要“修改”父组件的数据时,实际的做法不是修改 Props 本身,而是通过回调函数通知父组件。父组件将修改自身 State 的函数作为 Props 传递给子组件,子组件调用该函数并传入新的值,父组件更新 State 后通过新的 Props 将最新数据再次传递给子组件。这种“数据向下,事件向上”的模式是 React 数据管理的基础。
1 | function Parent() { |
这里 Child 没有修改 value Props——它调用 onChange 回调,让 Parent 自己更新状态。这一模式确保了数据流的单向性和可追溯性。
课后练习答案
一、概念自测答案
B
- 解析:Props 是从父组件传递给子组件的只读数据。A 描述的是 State;C 错误,函数组件也使用 Props;D 错误,Props 是输入而非输出。
B
- 解析:
children是 Props 的特殊属性,包含 JSX 标签的嵌套内容。A 错误(可传递任何可渲染内容);C 错误(可以有多个具名槽);D 错误(无需显式声明即可通过props.children访问)。
- 解析:
大写字母
- 解析:React 组件名必须以大写字母开头,以便 JSX 区分原生 HTML 标签和 React 组件。
A、C
- 解析:A 正确(具名槽模式);C 正确(Props 控制变体)。B 错误,React 推崇组合而非继承;D 错误,应通过 Props 定制变体而非复制组件。
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请用 React 实现一个通用 Modal 组件。要求:
- Props:
isOpen: boolean,onClose: () => void,title: string,children: ReactNode,footer?: ReactNode。isOpen为false时返回null。- 渲染半透明遮罩层,点击遮罩层调用
onClose。内容区点击时e.stopPropagation()阻止关闭。- 内容区包含
title、children、footer。- 在 App 中提供使用示例:一个按钮触发 Modal 显示/隐藏。
- 使用 TypeScript,
PropsWithChildren辅助类型。输出完整组件代码。”