[React 进阶系列] 组合组件 & 复合组件
今天写个人项目练手的时候搜到了一个比价有趣的实现,于是用了一下,发现这个 concept 不是特别的熟,于是上网找了下,返现了一个叫 复合组件(compound components) 的概念。搜索了一下后,发现 csdn 上关于这方面的比较少,很多搜出来的结果虽然写的是 复合组件,但是实际上的逻辑更像是 组合组件(composite components)
甚至是 deepseek 给出来的结果都有些混淆:
React 复合组件设计模式
复合组件是一种常见的 React 设计模式,它通过组合多个子组件来构建复杂的 UI 结构。这种模式的核心理念在于利用 props.children 和上下文传递数据的方式实现父子组件之间的通信。基本概念
复合组件模式允许父组件控制其子组件的行为和外观,而不需要直接操作这些子组件的状态或属性。这种方式增强了可重用性和灵活性 1。
使用场景
当需要创建一组紧密关联的组件时,可以采用此模式。例如,在表单库中,可能有一个 组件作为容器,其中包含若干输入字段(如 , 等)。每个字段都依赖于 提供的数据环境。
实现方式
以下是实现复合组件的一些关键点:
Context API: 利用 Context 来共享状态或者方法给所有的后代节点。
Render Props: 子组件可以通过 render prop 函数接收来自父级的信息并据此渲染自己的一部分视图逻辑。
下面是一个简单的例子展示如何使用 Composite Pattern 构建一个 Accordion(手风琴) 组件:
看这里的解释,核心概念还是用 composite pattern
而非 compound pattern
所以打算就这自己的理解写一下笔记,如果有对此比较了解到大佬可以更加深入的探讨学习一下就好了
大体总结一下就是:
Compound Components 通过共享状态的方式构建组件组
强调父组件对子组件的控制;
Composite Components 注重松耦合的组合与复用
复合组件 compound components
这个还是在搜索 colocation
这个关键词的时候慢慢从脑子里面跳出来,随后自己写了点东西出来,发现写出来的调用方法和之前记得一些 UI 库的使用方法很像,于是上网搜了下,发现了这个 design pattern
先说总结,compound components 的使用场景为:
- 子组件必须依附于父组件的 context 和 state
- 父子组件的逻辑非常清晰,其结构不应该被随意修改
- 子组件不可/不应该独立存在
目前用这个 pattern 比较多的库有
react-bootstrap
应该说 bootstrap 本身的设计思路就是基于 compound components 实现的
我找了下文档,目前来说一些表单类的还是比较依赖于 compound components,不过其他的一些实现,比如说 Grid 和 Stack 也是转向了 composite components 的设计
React Router
这不是个 UI 库,不过设计思路上是符合 compound components
Route 是不能够在 Routes 外实现的,并且 Route 的状态由 Routes 内部管理
formik
这个的表单管理还是依赖于父组件状态的
一些用的不是特别多的 UI 库,如 Radix UI, Semantic UI 之类的
大体的使用方法如下:
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";
function BasicExample() {
return (
<Form>
<Form.Group className="mb-3" controlId="FormBasicEmail">
<Form.Label>Email address</Form.Label>
<Form.Control type="email" placeholder="Enter email" />
<Form.Text className="text-muted">
We'll never share your email with anyone else.
</Form.Text>
</Form.Group>
<Form.Group className="mb-3" controlId="FormBasicPassword">
<Form.Label>Password</Form.Label>
<Form.Control type="password" placeholder="Password" />
</Form.Group>
<Form.Group className="mb-3" controlId="FormBasicCheckbox">
<Form.Check type="checkbox" label="Check me out" />
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
}
export default BasicExample;
这是从 react-bootstrap 上拉下来的一个案例,可以看到,其核心概念是:
子组件 必须 包括在父组件内
即有一个很明显的阶级结构,曾经 grid 也是这么实现的,
Grid.Col
必须是要在Grid
的结构目录下,如果不这么做,那么样式就会变得不太可控这也是为什么一些表单类的其实还是比较适合用这种结构,但是一些 UI 类的就不太适合了,毕竟
Grid.Col
和Flex.Col
的重复功能比较多对于开发者来说,嵌套 Grid 和 Flex 也会让代码的结构过于复杂,使得阅读性和管理都变得有些困难——特别是一些表单的业务逻辑特别复杂的情况下
子组件的状态会依赖于 context 或者父组件的状态
这个其实 formik、react router 也表单类的相关库可以看得出来
组件之间的耦合度很高
我现在工作的公司内部 UI 库,至少是支持 React 的这个,还是在使用 compound components,这也会导致一些情况下——需要嵌套 From、Grid、Flex 的情况,代码就挺乱的。而且我们其实对于 css 没什么办法去重写,一旦遇到一些问题,就只能继续增加嵌套,然后重写 css 去想办法实现用户的需求,这也是为啥会有多重嵌套的烦恼
因此我个人是觉得,除非出现业务逻辑真的有强关联的情况——如 form、router 这种,大多数情况下,普通的 UI 逻辑其实没有必要使用 compound components
我这次主要是想尝试一下实现功能,大体实现的业务逻辑如下:
import React from "react";
const StatGrid = ({
children,
columns = "grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4",
gap = "gap-7",
}) => {
return <div className={`w-full grid ${columns} ${gap}`}>{children}</div>;
};
const StatCard = ({
title,
subtitle,
icon: Icon,
cardBg = "#f5f5f5",
iconBg = "#333",
textColor = "#5c5a5a",
}) => {
return (
<div
className="flex justify-between items-center p-5 rounded-md gap-3"
style={{ backgroundColor: cardBg }}
>
<div
className="flex flex-col justify-start items-start"
style={{ color: textColor }}
>
<h2 className="text-3xl font-bold">{title}</h2>
<span className="text-md font-medium">{subtitle}</span>
</div>
<div
className="w-[40px] h-[47px] rounded-full flex justify-center items-center text-xl"
style={{ backgroundColor: iconBg }}
>
{Icon && <Icon className="text-[#fae8e8] shadow-lg" />}
</div>
</div>
);
};
StatGrid.Card = StatCard;
export default StatGrid;
其实从业务逻辑上来说,StatCard
与其父组件并没有构成绝对意义上的强关联,至少关联性没有强到需要用到 compound components 的程度,这个也只是想尝试性实验
除了上面写的,直接使用静态属性挂载的方法实现自组件,另一种写法更加的严苛,可以过滤掉所有不属于对应自组件的元素:
const StatGrid = ({ children }) => {
const cards = _.chain(React.Children.toArray(children))
.filter((child) => _.get(child, "type.displayName") === "StatGrid.Card")
.value();
return <div>{cards}</div>;
};
const StatCard = ({ children }) => <div>{children}</div>;
StatCard.displayName = "StatGrid.Card";
StatGrid.Card = StatCard;
组合组件 composite components
这是一个在 React 中非常常见的使用场景,React 官方文档也是更加推荐使用 composition 而不是 inheritance
事实上我个人感觉,大部分的 UI 库已经慢慢转向 composite components 的实现,毕竟这样的实现更佳的扁平化,而且这样的配置对 config array 的支持比较友好,总体来说 DX 体验感更好
一些比较常见的案例包括:
将 header,footer,body 组合,形成一个新的 wrapper 组件,并将其返回以减少代码的重复利用
通过嵌套一些第三方库提供的组件,形成一个 customized 的组件去使用,减少代码的重复性
比如说可以使用 react-icons + react-router-dom 提供的 Link 拼接成一个 clickable icon button
之前也提到了,记忆中 antd 和 MUI 还是使用 compound components 的,不过今天看了下最新的文档,应该说实现已经完全不一样了,其大体原因还是与复用性有关
如 antd/MUI 的 form 结构其实已经不需要依附于它们所提供的 Form
组件,而是让开发者自己去进行管理,这个时候更加扁平化的设计可以比较简单的添加、修改样式;真正的核心状态管理则可以让开发自己进行实现