☰
jstips 实战:用组合(Composition)增强你的 React 组件,彻底告别继承
2026/10/9 5:04:52 网站建设 项目流程
  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

在 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> ); }

这里有两个要点:

  1. List通过{props.children}把开始/结束标签之间的三个<li>原样渲染出来,自身完全不关心子元素的具体结构——这就是一个典型的“插槽”;
  2. 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 组件,正确姿势不是继承,而是组合。本文覆盖的两套模式可以直接套用:

  1. 预留插槽:容器组件通过props.children接收任意子内容,通过props接收其他定制元素(如header),把结构骨架与具体内容解耦;
  2. 通用化 + 特化:写一个能表达所有场景的通用组件,再为每个具体场景写一个薄封装(如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!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

相关推荐

上一篇:WarcraftHelper完整指南:魔兽争霸3终极免费辅助工具,彻底解决兼容性问题
下一篇:如何用Wand-Enhancer解锁WeMod完整功能:从限制到自由的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询