[React 进阶系列] 组合组件 复合组件

server/2025/3/25 22:04:23/

[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 之类的

大体的使用方法如下:

javascript">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.ColFlex.Col 的重复功能比较多

    对于开发者来说,嵌套 Grid 和 Flex 也会让代码的结构过于复杂,使得阅读性和管理都变得有些困难——特别是一些表单的业务逻辑特别复杂的情况下

  • 子组件的状态会依赖于 context 或者父组件的状态

    这个其实 formik、react router 也表单类的相关库可以看得出来

  • 组件之间的耦合度很高

我现在工作的公司内部 UI 库,至少是支持 React 的这个,还是在使用 compound components,这也会导致一些情况下——需要嵌套 From、Grid、Flex 的情况,代码就挺乱的。而且我们其实对于 css 没什么办法去重写,一旦遇到一些问题,就只能继续增加嵌套,然后重写 css 去想办法实现用户的需求,这也是为啥会有多重嵌套的烦恼

因此我个人是觉得,除非出现业务逻辑真的有强关联的情况——如 form、router 这种,大多数情况下,普通的 UI 逻辑其实没有必要使用 compound components

我这次主要是想尝试一下实现功能,大体实现的业务逻辑如下:

javascript">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 (<divclassName="flex justify-between items-center p-5 rounded-md gap-3"style={{ backgroundColor: cardBg }}><divclassName="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><divclassName="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 的程度,这个也只是想尝试性实验

除了上面写的,直接使用静态属性挂载的方法实现自组件,另一种写法更加的严苛,可以过滤掉所有不属于对应自组件的元素:

javascript">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 组件,而是让开发者自己去进行管理,这个时候更加扁平化的设计可以比较简单的添加、修改样式;真正的核心状态管理则可以让开发自己进行实现


http://www.ppmy.cn/server/179071.html

相关文章

RISC-V: 固件与操作系统引导 | eg OpenSBI | 借助AI注释项目代码

引入&#xff1a;计算机没有黑魔法 例如我们都可以&#xff0c;通过指令来查看我们计算机的信息 “Everything is a State Machine” 在许多状态之间不断切换程序就运行了起来Makefile 也是程序&#xff1b;它也是状态机程序不好读的话&#xff0c;我们还可以调试它&#xff0…

Language Models are Few-Shot Learners,GPT-3详细讲解

GPT的训练范式&#xff1a;预训练Fine-Tuning GPT2的训练范式&#xff1a;预训练Prompt predict &#xff08;zero-shot learning&#xff09; GPT3的训练范式&#xff1a;预训练Prompt predict &#xff08;few-shot learning&#xff09; GPT2的性能太差&#xff0c;新意高&…

国产开发板—米尔全志T113-i如何实现ARM+RISC-V+DSP协同计算?

近年来&#xff0c;随着半导体产业的快速发展和技术的不断迭代&#xff0c;物联网设备种类繁多&#xff08;如智能家居、工业传感器&#xff09;&#xff0c;对算力、功耗、实时性要求差异大&#xff0c;单一架构无法满足所有需求。因此米尔推出MYD-YT113i开发板&#xff08;基…

Unity音频混合器如何暴露参数

音频混合器是Unity推荐管理音效混音的工具&#xff0c;那么如何使用代码对它进行管理呢&#xff1f; 首先我在AudioMixer的Master组中创建了BGM和SFX的分组&#xff0c;你也可以直接用Master没有问题。 这里我以BGM为例&#xff0c;如果要在代码中进行使用就需要将参数暴露出去…

linux去掉绝对路径前面部分和最后的/符号

使用basename命令 basename命令用于获取路径中的文件名部分。它会自动去除路径前面的目录部分和最后的/符号。示例如下&#xff1a; path"/a/b/c" filename$(basename "$path") echo "$filename"path"/a/b/c/" filename$(basename &…

【测试工具】如何使用 burp pro 自定义一个拦截器插件

在 Burp Suite 中&#xff0c;你可以使用 Burp Extender 编写自定义拦截器插件&#xff0c;以拦截并修改 HTTP 请求或响应。Burp Suite 支持 Java 和 Python (Jython) 作为扩展开发语言。以下是一个完整的流程&#xff0c;介绍如何创建一个 Burp 插件来拦截请求并进行自定义处理…

Spring-Mybatis框架常见面试题

1、介绍下什么是Spring框架的IOC和DI IOC 控制反转&#xff0c;指将对象的创建权&#xff0c;反转到Spring容器&#xff1b; DI 依赖注入&#xff0c;指Spring创建对象的过程中&#xff0c;将对象依赖属性通过配置进行注入,不能单独存在&#xff0c;需要在IOC的基础上完成操作…

linux更换镜像源[CentOs]

问题&#xff1a;在使用linux的yum命令时常常会遇到由于无法加载到centos官方镜像源的问题&#xff0c;报错信息如图所示 解决方法&#xff1a;更换国内的数据源 1. 备份原有仓库配置 sudo cp -r /etc/yum.repos.d/ /etc/yum.repos.d.backup # 备份整个目录 sudo rm -rf /et…