- 教程
【免费下载链接】jstips
This is about useful JS tips!
在 Facebook,我们使用上千个 React 组件,却找不到任何推荐你使用组件继承(inheritance)的场景。—— React 官方团队
Long live to composition, inheritance is dead.(组合万岁,继承已死。)
这是一条来自 jstips 社区的第 69 号技巧(英文原版见 _posts/en/react/2017-04-04-enhancing-react-components-composition.md),它回答了 React 开发者最常纠结的问题:“不继承,我该怎么复用/扩展一个组件?”本文将基于原文档的两套核心模式——props.children插槽组合与“通用组件 + 特化”模式,完整还原代码示例,并结合仓库中其他 React 技巧(如 key 的正确使用、state→props 映射优化)做纵深补充。读完你就能掌握:如何用数据流和插槽结构替代类继承,写出更灵活、更易维护的 React 组件。
为什么 React 不推荐组件继承
原文档开篇引用了 Facebook 团队的原话:他们在数千个组件中都没有找到需要建立组件继承层次的场景。背后的原因很直白:
- 继承建立的是静态的“is-a”关系,父类一旦定型,子类扩展受限于父类结构,改动父类往往会波及其他子类;
- 组合建立的是动态的“has-a”关系,组件通过
props接收外部注入的内容与行为,父组件与子组件通过数据接口解耦,替换、复用、测试都更轻松; - React 的数据流是单向的,
props天然承担“配置注入”的职责,这让组合成为与框架理念天然契合的组织方式。
所以当你想“扩展”一个组件时,思路应从“继承它的类”转换为“在它周围或内部放置内容”。下面进入原文档给出的两种落地模式。
模式一:提前规划——用props.children留出插槽
在某些场景下,你有一个容器组件,但事先并不知道它的 children(子内容)会是什么。React 为此提供了保留属性props.children:JSX 中组件开始标签与结束标签之间的所有元素,都会作为children传入。
原文档的第一个示例是通用列表组件,它只关心容器的className,不关心里面装了什么:
function List(props) { return ( <ul className={'List List-' + props.importance}> {props.children} </ul> ); } function DownloadMenu() { return ( <List importance="High"> <li>Download SBCL</li> <li>Download Racket</li> <li>Download Haskell</li> </List> ); }这里有两个要点:
List通过{props.children}把开始/结束标签之间的三个<li>原样渲染出来,自身完全不关心子元素的具体结构——这就是一个典型的“插槽”;importance="High"通过props传入并拼进className,用于控制样式变体。importance的取值可以按你的设计约定为"High"/"Medium"/"Low"等,组件内部只做字符串拼接,具体取值与样式类的对应关系由 CSS 层决定。
用 props 填补空白:插槽之外的定制点
如果插槽还不够,你可以更进一步:把children当作众多“可填充空洞”之一,其余空洞也用props注入。原文档的第二个示例在列表顶部加了一个可选的“表头”插槽:
function ListWithHeader(props) { return ( <ul className={'List List-' + props.importance}> <li className="List List-Header"> {props.header} </li> {props.children} </ul> ); } function DownloadMenuWithHeader() { return ( <List importance="High" header={ <LispLogo /> }> <li>Download SBCL</li> ... </List> ); }注意header={ <LispLogo /> }:props.header接收的可以是任意 React 元素,甚至是另一个组件实例。这样设计的好处是:
- 单一职责:
ListWithHeader只负责布局骨架,LispLogo是什么完全由使用方决定; - 可组合:
header可以是文本、图标、按钮、甚至是整块复杂组件,无需修改ListWithHeader本身; - 渲染位置可控:
props.header被放在列表的最前面,与props.children(主体内容)形成两个独立的渲染出口,调用方只需关心“给什么”,不需要关心“放在哪”。
模式二:通用组件与特化(Generic components and specialization)
当你有多个形态相似、细节不同的界面时,不必为每个变体复制粘贴一份组件,而是写一个通用组件,再用不同 props 特化出具体场景。原文档以一个文件系统视图FolderView为例:
function FolderView(props) { return ( <div className="FolderView"> <h1>{props.folderName}</h1> <ul className="FolderView FolderView-Actions"> {props.availableActions} </ul> <ul className="FolderView FolderView-Files"> {props.files} </ul> </div> ); }FolderView能表达文件系统里的任意文件夹:名称来自folderName,可用操作来自availableActions,文件列表来自files。它不知道也不关心“这是桌面还是图片文件夹”——特化逻辑交给调用方:
function DesktopFolder(props) { return ( <FolderView folderName="Desktop" availableActions={ <li>Create Folder</li> <li>Create Document</li> } files={props.files} /> ); } function PicturesFolder(props) { return ( <FolderView folderName="Pictures" availableActions={ <li>New Picture</li> } files={props.files} /> ); }就是这样:我们特化了组件,却没有建立任何继承层次。对比继承方案可以看到组合模式的优势:
| 维度 | 继承方案 | 组合方案(本文) |
|---|---|---|
| 变体实现 | 为每个变体写子类 | 为每个变体写一个薄封装函数 |
| 复用单元 | 类方法、属性 | 可注入的 props 与元素插槽 |
| 变更影响 | 父类改动可能波及全部子类 | 通用组件改动仅影响其自身渲染逻辑 |
| 扩展方式 | 需要新增子类 | 只需传不同的 props / children |
DesktopFolder与PicturesFolder本质上是“配置工厂”:它们各自把folderName、availableActions等参数绑定好,再统一渲染同一个FolderView。新增一种文件夹类型,只需再写一个类似的薄封装,通用组件一行都不用动。
组合模式的底层支撑与配套注意点
children 到底是什么
从机制上讲,props.children是 React 组件的一个特殊保留属性,JSX 编译器会把开始/结束标签之间的内容编译为children传入。因此:
- children 可以是单个元素、元素数组、字符串、数字,甚至是函数(render props 场景);
- 当调用方什么都不传时,
props.children为undefined,容器组件可以通过条件渲染决定是否显示占位内容; - 容器组件本身不渲染 children,它只负责“放置” children——放置的位置、顺序、是否包一层结构,完全由容器组件决定。
组合大量列表内容时,别忘了key
当props.children是通过数组动态生成的大量元素时,需要为每一项提供稳定唯一的key。这一点在本仓库的另一条 React 技巧 _posts/en/react/2016-01-02-keys-in-children-components-are-important.md 中有专门论述:key 是 React 用来判断“同一组件还是不同组件”的身份标识,应使用对象中已有的唯一值,而不要用数组索引或Math.random(),否则会导致子组件被错误重建、状态丢失等诡异问题。把“组合的插槽结构”与“正确的 key”搭配使用,才能在大型列表场景下既灵活又高效。
与数据流优化结合:组合负责结构,selector 负责数据
组合解决的是“组件结构如何组织”,而组件拿到的props往往来自容器组件的映射计算。本仓库的 _posts/en/react/2017-03-27-state-to-props-maps-with-memory.md 展示了:用 reselect 的createSelector给mapStateToProps中的计算函数加上“记忆”,只有相关 state 片段变化时才重新计算。把这条经验套进本文的FolderView场景,就是让files、availableActions的派生计算具备记忆化,避免无关 state 变化引发无谓重算——结构上组合、数据上缓存,两者相得益彰。
小结:把“继承”换成“配置”
回到原文档的核心结论:想“扩展”一个 React 组件,正确姿势不是继承,而是组合。本文覆盖的两套模式可以直接套用:
- 预留插槽:容器组件通过
props.children接收任意子内容,通过props接收其他定制元素(如header),把结构骨架与具体内容解耦; - 通用化 + 特化:写一个能表达所有场景的通用组件,再为每个具体场景写一个薄封装(如
DesktopFolder、PicturesFolder),只传参数、不建层次。
这套模式在本仓库中被记录为第 69 号技巧,位于 _posts/zh_TW/react/2017-04-04-enhancing-react-components-composition.md,对应英文原版 _posts/en/react/2017-04-04-enhancing-react-components-composition.md,并且在 README.md 的 tips 列表(第 40 行)中正式收录。如果你想继续深入 React 的组织与性能话题,可以接着阅读仓库中的 _posts/en/react/2017-04-10-adventurers-guide-to-react.md(React 工具链入门)与 _posts/en/react/2017-05-29-upping-performance-by-appending-keying.md(渲染性能优化)。
- 教程
【免费下载链接】jstips
This is about useful JS tips!
相关推荐
JS Tips 第 69 期:用组合(Composition)增强 React 组件,告别继承
JS Tips 第 69 期:用组合(Composition)增强 React 组件,告别继承 在 Facebook,我们使用 React 构建了成千上万个组件
教程AIO Sandbox性能优化:10个提升沙箱效率的技巧
AIO Sandbox性能优化:10个提升沙箱效率的技巧 AIO Sandbox是一个为AI智能体设计的 一体化沙箱环境 ,它将浏览器自动化、终端操作、文件管理
AI Agent后端MCP 服务浏览器控制Agent 评测告别混乱继承:Vue.js组件复用的Mixins与Composition API终极对决
告别混乱继承:Vue.js组件复用的Mixins与Composition API终极对决 Vue.js作为现代前端开发的主流框架,其组件复用机制一直是开发者关注
前端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考