---
tags: [React, Components, Props, Frontend]
create time: 2026-04-29 22:02
---
# 组件与 Props
## 概述
React 应用由一组可复用的组件构成。理解组件的拆分粒度、Props 的类型安全传递,以及组合优于继承的设计哲学,是写出高质量 React 代码的核心。本章从组件的基本形态讲起,深入到 Props 的校验与传递模式,最后介绍组件组合的高级技巧。
> [!tip] 本节学习路线
> 1. 函数组件 vs 类组件 → 为什么函数组件成为主流
> 2. 组件拆分原则 → 如何找到合理的职责边界
> 3. Props 定义与校验 → TypeScript 方式(推荐)和 PropTypes 方式
> 4. Props 传递模式 → 基础透传、回调通信、children 插槽
> 5. Composition 高级模式 → React.memo、forwardRef、受控组件
> 6. Context 初探 → 轻量级跨层级通信
## 函数组件 vs 类组件
```tsx
// ✅ 函数组件(当前推荐写法)
// 输入 props,输出 JSX —— 纯函数的直观映射
function Greeting({ name }: { name: string }) {
return
Hello, {name}
;
}
// ⚠️ 类组件(遗留项目常见,新项目中应避免)
// this.props + render() —— 需要额外处理 this 绑定
class Greeting extends React.Component<{ name: string }> {
render() {
return Hello, {this.props.name}
;
}
}
```
### 为什么推荐函数组件?
| 对比维度 | 函数组件 + Hooks | 类组件 |
|----------|------------------|--------|
| 心智模型 | 纯函数:输入 props → 输出 JSX | 实例方法 + this 绑定 |
| 逻辑复用 | Custom Hooks(声明式组合) | HOC / Render Props(嵌套地狱)|
| 类型推导 | TS 自动推断完善 | 需要手动泛型参数 |
| 性能开销 | 无实例创建成本 | new 实例 + bind overhead |
| Suspense | 完全支持 | 不支持 |
| Hooks 绑定 | 基于调用顺序 | 依赖 this 上下文 |
> [!note] Hooks 绑定的本质
> useState、useEffect 等 Hook 按调用顺序内部维护一个链表。这就是为什么 Hook **不能写在条件语句或循环中**——顺序一旦改变,状态就会错配。
## 组件拆分原则
> [!question] 思考:一个按钮算一个独立组件,还是把按钮和其外层容器一起写?
>
> 如果按钮只在这一处使用,合并更简洁;如果多个地方共用同一个按钮样式或交互逻辑,就应该拆出来。核心判断标准:**这个组件是否有独立的业务语义?**
好的拆分遵循 **"单一职责" + "合理抽象"**。下面用流程图展示不同层级的职责划分:
```mermaid
graph LR
subgraph P ["Page Level — 页面组装层"]
A["AboutPage
拼接各功能模块"]
end
subgraph F ["Feature Level — 功能模块层"]
B["UserProfile
头像 + 昵称 + 设置入口"]
C["Dashboard
图表 + 数据表格"]
end
subgraph C2 ["Component Level — 通用 UI 层"]
D["Button
按钮变体 & 尺寸"]
E["Modal
弹窗外壳"]
end
subgraph PL ["Primitive Level — 原子层"]
F2["IconButton
图标按钮"]
G["Avatar
头像图片"]
end
A --> B
A --> C
B --> D
B --> G
C --> E
D --> F2
```
### 判断是否该拆分的标准
1. **可复用性** — 同一组 JSX 出现两次以上,考虑抽取为独立组件
2. **可读性** — 单个文件超过 300 行、单个组件超过 80 行,应考虑拆分
3. **测试粒度** — 难以单独测试的巨型组件应拆为小组件
4. **业务语义** — 每个组件应对应一个清晰的业务概念
```tsx
// ❌ 反例:巨型组件,混合了多个不相关的职责
function Dashboard() {
// 200+ 行,混合了 Header、Sidebar、DataTables、Chart...
// 改 Header 要遍历整文件,测试也要 mock 所有子模块
}
// ✅ 正例:拆分为功能块,每个组件职责单一
function Dashboard() {
return (
<>
{/* 专注头部信息 */}
{/* 专注侧边导航 */}
{/* 专注数据表格 */}
{/* 专注图表展示 */}
>
);
}
```
## Props 定义与类型校验
### TypeScript 接口方式(推荐)
```tsx
interface ButtonProps {
label: string; // 必填:按钮文字
variant?: "primary" | "secondary" | "danger"; // 可选:视觉变体(联合类型约束)
size?: "sm" | "md" | "lg"; // 可选:尺寸档位
disabled?: boolean; // 可选:禁用态
onClick?: (e: React.MouseEvent) => void; // 可选:点击回调
children?: React.ReactNode; // 可选:内容插槽
}
// 方式1:解构赋值设默认值(适合简单默认值)
function Button({ label, variant = "primary", onClick }: ButtonProps) {
return ;
}
// 方式2:全部字段显式声明默认值(适合需要覆盖全部可选字段时)
function Button({ label, variant = "primary", size = "md", disabled = false, children }: ButtonProps) {
return ;
}
```
> [!tip] 类型定义的位置建议
> - 小型组件:interface 与组件放在同一文件中
> - 大型组件库:type 导出到独立的 `types.ts`,便于跨文件引用和复用
### PropTypes 方式(JS 项目或过渡期)
```js
import PropTypes from 'prop-types';
Button.propTypes = {
label: PropTypes.string.isRequired, // 必填字符串
variant: PropTypes.oneOf(["primary", "secondary"]), // 枚举限制
onClick: PropTypes.func, // 可选函数
};
```
> [!warning] 不要混用两种校验方式
> TS 提供**编译时检查**,PropTypes 是**运行时兜底**。选其一即可,新项目统一用 TS。混用会导致维护和调试成本翻倍。
## Props 传递模式
### 基础传递
```tsx
// 1. 直接传递字面量值
// 2. 传递变量或表达式
```
### 展开透传(Spread Props)
```tsx
// 当一层组件需要将自身 props 原样传给子组件时,避免逐个手写
const inputProps = { placeholder: "请输入", onChange: handleChange };
// ⚡ Spread Operator 将对象解包为独立的 props
// 局部覆盖:父 prop 优先级低于子 prop(后者覆盖前者)
// autoFocus 额外传入,其余来自 inputProps
```
### 回调 Prop(子 → 父通信)
```tsx
// 子组件通过回调将数据或事件通知父组件