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

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

&lt;think&gt;好的,用户想了解组件库开发的最佳实践、设计模式和工具推荐。首先,我需要整理组件库开发的关键点。根据用户提供的引用内容,设计模式在C语言中都能应用,虽然C不是面向对象,但结构化和模块化很重要。引用[1]提到设计模式改善代码结构和模块化,引用[2]提到设计原则如单一职责,这些都可以应用到组件库中。 组件库开发需要模块化设计,每个组件独立,遵循单一职责原则。这可能涉及到工厂模式或策略模式,用来解耦组件创建和逻辑。比如工厂模式可以用于动态创建组件实例,而策略模式处理不同行为。 然后是文档和类型定义,好的文档和TypeScript支持能提升可维护性。测试方面,单元测试和自动化测试工具如Jest、Cypress很重要,引用[3]提到自动测试和持续集成,这部分需要强调。 工具推荐方面,构建工具如Webpack、Rollup,样式工具Sass,还有Storybook展示组件。版本控制用Git,CI/CD工具如GitHub Actions,这些都能提高效率。 设计模式部分,组合模式适合构建复杂组件,装饰模式增强功能,观察者模式处理事件通信。最佳实践包括版本管理、代码规范、按需加载和主题定制。 最后,生成相关问题时要覆盖设计模式的选择、性能优化、文档维护、测试策略、样式管理和跨平台开发等方面。确保每个部分都有引用支持,比如设计模式参考[^1][^2],自动化测试参考[^3]。&lt;/think&gt;### 组件库开发指南 #### 一、组件库开发核心原则 1. **模块化设计** 每个组件应独立封装功能,遵循**单一职责原则**[^2],例如按钮组件仅处理交互逻辑,样式与行为分离。 ```typescript // 示例:React按钮组件 interface ButtonProps { label: string; onClick: () =&gt; void; variant?: &#39;primary&#39; | &#39;secondary&#39;; } const Button = ({ label, onClick, variant = &#39;primary&#39; }: ButtonProps) =&gt; ( &lt;button className={`btn-${variant}`} onClick={onClick}&gt;{label}&lt;/button&gt; ); ``` 2. **设计模式应用** - **工厂模式**:动态创建组件实例(如根据配置生成不同表单控件) - **策略模式**:实现组件行为的灵活切换(如验证逻辑的动态替换)[^2] #### 二、最佳实践流程 1. **架构设计** ```mermaid graph TD A[核心层-基础组件] --&gt; B[业务层-复合组件] B --&gt; C[应用层-页面模板] ``` 2. **开发工具链** | 工具类型 | 推荐方案 | 核心功能 | |----------------|-------------------------|--------------------------| | 构建工具 | Webpack/Rollup | 代码打包与Tree Shaking | | 样式处理 | Sass/Less + CSS Modules | 样式隔离与变量管理 | | 文档系统 | Storybook/Docusaurus | 组件演示与API文档生成 | 3. **质量保障体系** - **单元测试**:使用Jest + Testing Library(覆盖率&gt;80%) - **E2E测试**:Cypress/Puppeteer验证完整交互流程[^3] - **持续集成**:GitHub Actions自动运行测试套件[^3] #### 三、关键设计模式详解 1. **组合模式(Compound Components)** 通过上下文共享状态,实现父子组件深度集成: ```jsx &lt;Tabs&gt; &lt;Tabs.List&gt; &lt;Tabs.Tab&gt;Tab1&lt;/Tabs.Tab&gt; &lt;Tabs.Tab&gt;Tab2&lt;/Tabs.Tab&gt; &lt;/Tabs.List&gt; &lt;Tabs.Panels&gt; &lt;Tabs.Panel&gt;Content1&lt;/Tabs.Panel&gt; &lt;Tabs.Panel&gt;Content2&lt;/Tabs.Panel&gt; &lt;/Tabs.Panels&gt; &lt;/Tabs&gt; ``` 2. **观察者模式** 组件间通过事件总线通信,实现解耦: ```typescript class EventBus { private listeners = new Map&lt;string, Function[]&gt;(); emit(event: string, data?: any) { this.listeners.get(event)?.forEach(fn =&gt; fn(data)); } } ``` #### 四、进阶优化策略 1. **按需加载** 使用动态导入实现代码分割: ```js const Modal = React.lazy(() =&gt; import(&#39;./Modal&#39;)); ``` 2. **主题系统** 通过CSS变量实现动态换肤: ```css :root { --primary-color: #1890ff; --text-size: 14px; } .component { color: var(--primary-color); } ``` 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值