--- 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. 直接传递字面量值 ; } ``` > [!warning] Context 的性能陷阱 > - Provider value 如果是**新对象**,每次渲染都是不同的引用 → 所有消费者全部重渲染 > - ✅ 解决:用 `useReducer` 返回稳定的 `{state, dispatch}` 对象,保持引用一致 > - ✅ 解决:拆分 Context,避免一个大 Context 塞入过多数据 ## 组件组合的进阶模式 ### Compound Components(复合组件) 当一个组件的子项之间存在**隐式共享状态**时,使用复合组件模式: ```tsx // 手风琴组件:Tab 之间共享 activeIndex,但父组件无需关心 function Accordion({ children }: { children: React.ReactNode }) { const [activeIndex, setActiveIndex] = useState(null); return (
{children}
); } function AccordionTab({ title, children }: { title: string; children: React.ReactNode }) { const ctx = useContext(AccordionContext); // 💡 每个 Tab 自己从 Context 里拿状态,父组件不用逐一传递 const isActive = ctx.activeIndex !== null; return (
ctx.setActiveIndex(ctx.activeIndex)}>

{title}

{isActive &&
{children}
}
); } // 对外暴露子组件属性,方便用户使用 Accordion.Tab = AccordionTab; // 使用:状态在 Tabs 间共享,父组件只需包裹 ... ... ``` > [!question] 什么时候用 compound components? > 当你的一组子组件需要**彼此感知对方的状态**,又不想让父组件来协调时。典型场景:Tabs/Accordion/SwitchGroup/Table。 ### Composition Flow ```mermaid sequenceDiagram participant Parent as 父组件 participant Child as 子组件 participant DOM as DOM Parent->>Child: 传入 props + children(JSX 树) Note over Parent,Child: Props 描述配置,Children 描述内容 Child->>DOM: 根据 props 渲染特定结构 Child->>Child: 插入 children 到指定位置 Note over Child: 组合 = 配置 + 内容的分离 ``` > [!tip] 组合 vs 继承 > React 的设计哲学是 **组合优于继承**: > - JS 的 class 继承会导致紧耦合和脆弱的基类依赖 > - React 组合让每个组件保持独立,行为通过 props/children/Callbacks 灵活拼装 > - 💡 如果想共享逻辑,优先考虑 Custom Hooks 而非继承 ## 关联笔记 - [[REACT/1. 基础篇/01-环境搭建与项目结构]] — React 项目初始化与目录规范 - [[REACT/1. 基础篇/02-JSX 语法]] — JSX 表达式、事件绑定、条件渲染 - [[REACT/1. 基础篇/04-State 与不可变性]] — State 设计原则与更新模式 - [[REACT/2. Hooks 篇/05-核心 Hooks]] — useState/useEffect/useContext/useRef 原理 - [[REACT/2. Hooks 篇/07-自定义 Hooks]] — 逻辑复用与常见 Hook 封装 - [[REACT/4. 进阶篇/11-组件通信模式]] — Props Drill / Context / 状态管理库方案对比 - [[REACT/3. 生态工具篇/09-状态管理]] — Context API vs Zustand vs Redux Toolkit - [[REACT/5. 工程实践篇/15-性能优化]] — React.memo / 虚拟列表 / Code Splitting