React: 同事见了直夸夸的复合组件模式

2024-09-11
14615 分钟
...

在React的开发中,我们经常会遇到这样的场景:组件之间需要共享状态,并且根据用户的交互或业务需求,多个组件需要协同工作。这时,设计出既灵活又可维护的组件结构就变得非常重要。复合组件模式 (Compound Component Pattern) 就是一种很好的设计模式,它允许父组件提供共享的状态,子组件则负责完成具体的任务。

概念

复合组件模式指的是,将一个功能拆分成多个子组件,通过上下文(Context)进行状态共享,父组件负责提供核心逻辑,子组件则根据需要访问状态或操作函数。它让父子组件的通信更加直观,也能大大提升组件的灵活性与可扩展性。

使用场景

1. 可重用的交互组件

当我们需要构建一个交互组件,如模态窗口,标签页或者表单时,这些组件往往由多个部分组成,例如模态窗口: 展示窗口,背景模态,打开和关闭窗口的组件等。在这种情况下,复合组件模式非常适用,它让你能够以一种声明式的方式构建组件,同时保持各个部分的分离。

2. 需要灵活组合的组件

有些场景下,用户可能需要以不同的方式组合和使用组件。例如,在表单中,输入框和按钮是常见的组合,但不同页面可能对这些组件的布局或功能有不同的要求。复合组件模式允许开发者灵活地组合这些子组件,轻松适应各种需求。

为什么使用复合组件模式

1. 提升组件的灵活性

复合组件模式允许开发者根据具体的使用场景来组合子组件。每个子组件专注于完成自己的任务,而父组件负责管理状态。这种解耦设计让组件更加灵活,父组件可以随意调整子组件的数量或结构。

2. 避免 prop drilling(属性层层传递)

传统的组件通信往往需要通过一层层地传递属性,这种方式不仅繁琐,还会导致组件之间的耦合度过高。复合组件模式通过使用 Context 来共享状态,避免了这种属性传递的麻烦,子组件可以直接访问需要的状态或方法。

3. 增强可维护性

组件拆分清晰,逻辑职责分明,使得代码更加易于维护。如果某个功能需要调整,开发者只需修改对应的子组件,而不必担心对整个组件造成影响。

实践展示

我以一个模态窗口为例来展示。正常来讲一般会是这么构建一个模态窗口组件:





function Modal({ children, onClose }) {
  return createPortal(
    <Overlay>
      <StyledModal>
        <Button onClick={onClose}>
          <HiXMark />
        </Button>

        <div>{children}</div>
      </StyledModal>
    </Overlay>,
    document.body
  );
}

export default Modal;

在外部的使用方法则如下所示:

function AddUser() {
  const [isOpenModal, setIsOpenModal] = useState(false);

  return (
    <div>
      <Button onClick={() => setIsOpenModal((show) => !show)}>
        添加成员
      </Button>
      {isOpenModal && (
        <Modal onClose={() => setIsOpenModal(false)}>
          <CreateUserForm onCloseModal={() => setIsOpenModal(false)} />
        </Modal>
      )}
    </div>
  );
}

export default AddUser;

乍一看的话这样写也很不错了,适用了大部分场景,但是当它和别的组件套用时,就会显得很乱,并且假如放在一个选项列表中,连续几个按钮都要打开不同的窗口时,就会显得不那么够用,逻辑不那么清晰了。

下面给出复合组件模式的模态窗口写法:


const ModalContext = createContext();


function Modal({ children }) {
  const [openName, setOpenName] = useState("");

  const close = () => setOpenName("");
  const open = setOpenName;

  return (
    <ModalContext.Provider value={{ openName, close, open }}>
      {children}
    </ModalContext.Provider>
  );
}


function Open({ children, opens: opensWindowName }) {
  const { open } = useContext(ModalContext);
  
  
  return cloneElement(children, { onClick: () => open(opensWindowName) });
}

function Window({ children, name }) {
  const { openName, close } = useContext(ModalContext);
  const ref = useOutsideClick(close);
  
  
  if (name !== openName) return null;

  return createPortal(
    <Overlay>
      <StyledModal ref={ref}>
        <Button onClick={close}>
          <HiXMark />
        </Button>

        <div>{cloneElement(children, { onCloseModal: close })}</div>
      </StyledModal>
    </Overlay>,
    document.body
  );
}


Modal.Open = Open;
Modal.Window = Window;

export default Modal;

那么这样的使用方法就变成了如下方式:

function AddUser() {
  return (
    <div>
      <Modal>
        <Modal.Open opens="user-form">
          <Button>添加成员</Button>
        </Modal.Open>
        
        <Modal.Window name="user-form">
          <CreateUserForm />
        </Modal.Window>
        
        {/* 可以添加更多的按钮以打开对应窗口 */}
        {/* <Modal.Open opens="table">
          <Button>展示成员</Button>
        
        </Modal.Open>
        <Modal.Window name="table">
          <UserTable />
        </Modal.Window> */}
      </Modal>
    </div>
  );
}

export default AddUser;

再例如,我们与一个表格组件 (表格同样为复合模式写法) 去套用模态窗口:

function CabinRow({ cabin }) {
  ...
  return (
    <Table.Row>
      ...
      <div>
        <Modal>
          <Menus.Menu>
            <Menus.Toggle id={cabinId} />

            <Menus.List id={cabinId}>
              <Menus.Button
                icon={<HiSquare2Stack />}
                onClick={handleDuplicate}
                disabled={isCreating}
              >
                Duplicate
              </Menus.Button>

              <Modal.Open opens="edit">
                <Menus.Button icon={<HiPencil />}>Edit</Menus.Button>
              </Modal.Open>

              <Modal.Open opens="delete">
                <Menus.Button icon={<HiTrash />}>Delete</Menus.Button>
              </Modal.Open>
            </Menus.List>
          </Menus.Menu>

          <Modal.Window name="edit">
            <CreateCabinForm cabinToEdit={cabin} />
          </Modal.Window>

          <Modal.Window name="delete">
            <ConfirmDelete
              resourceName="cabins"
              disabled={isDeleting}
              onConfirm={() => deleteCabin(cabinId)}
            />
          </Modal.Window>
        </Modal>
      </div>
    </Table.Row>
  );
}

export default CabinRow;

最终可以达成这样的效果,同时逻辑也很清晰明了

表格组件代码如下:

const TableContext = createContext();

function Table({ columns, children }) {
  return (
    <TableContext.Provider value={{ columns }}>
      <StyledTable role="table">{children}</StyledTable>
    </TableContext.Provider>
  );
}

function Header({ children }) {
  const { columns } = useContext(TableContext);
  return (
    <StyledHeader role="row" columns={columns} as="header">
      {children}
    </StyledHeader>
  );
}

function Row({ children }) {
  const { columns } = useContext(TableContext);
  return (
    <StyledRow role="row" columns={columns}>
      {children}
    </StyledRow>
  );
}

function Body({ data, render }) {
  if (!data.length) return <Empty>暂无数据展示</Empty>;

  
  return <StyledBody>{data.map(render)}</StyledBody>;
}

Table.Header = Header;
Table.Body = Body;
Table.Row = Row;
Table.Footer = Footer;

export default Table;

复合组件模式的优点总结

  • 高灵活性:开发者可以自由组合子组件,扩展或修改子组件也很方便。
  • 清晰的职责划分:子组件只关心自己负责的功能,父组件负责提供状态,组件职责明确。
  • 简化状态共享:通过 Context 共享状态,避免了繁琐的属性传递,提高了代码的可读性和可维护性。
  • 便于扩展:新功能可以通过添加子组件来实现,父组件无需修改,符合开闭原则(OCP)。

结语

复合组件模式为 React 组件的设计提供了一种灵活、优雅的解决方案,特别适用于需要多个组件协同工作的场景。它不仅提升了组件的可复用性,还让代码更加清晰、易于维护。如果你在开发过程中遇到组件拆分和状态共享的需求,不妨试试复合组件模式,相信它能让你的组件设计更加简洁、强大。

希望本文能够帮助你理解并使用复合组件模式,打造更灵活、可维护的 React 组件体系。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
react2025-12-02
useEffect与useLayoutEffect对比
在 React Hooks 中, 和 都用于处理副作用逻辑,例如订阅、事件监听、DOM 操作等。但它们有着本质性的执行时机差异,这影响到页面渲染、布局抖动、性能表现等关键点。 1\. 执行时机的核心区别 在 React 的渲染流程中: 1. 渲染(Render phase) React 根据 state/props 计算 UI,生成虚拟 DOM,不访问真实 DOM。 2. 提交(Commit phase) React 将虚拟 DOM 变...
学习
react2025-08-03
在 React 中实现倒计时功能会有什么坑
倒计时 倒计时是一个非常常见的业务场景,但是在 React 中实现起来,却不算简单。 首先我们来看这段倒计时代码,它能否正常执行? 来看看实际表现效果 可以看到计时器在不断执行,但是 的值却没有变。 这是一个很经典的问题:React 闭包陷阱 。 我们来详细分析下: 的依赖数组 是空的,这意味着 effect 只会在组件挂载时执行一次,而不会在 状态更新时重新执行。所以 回调函数中捕获的 值始终是初始值 。 这个问题解决起来也很简单,有...
学习面试
react2025-07-30
React性能优化三剑客:memo、useMemo和useCallback详解
前言 在React开发中,性能优化是一个永恒的话题。今天我们就来深入探讨React提供的三个重要性能优化工具: 、 和 ,它们如何帮助我们构建更高效的React应用。 1\. 为什么需要性能优化? 🤔 React的核心机制是当组件的state或props发生变化时,组件会重新渲染。但有时这种重新渲染是不必要的: 父组件更新导致所有子组件重新渲染 ,即使子组件的props没有变化 复杂计算在每次渲染时重复执行 ,消耗大量资源 函数引用在...
学习面试
react2025-06-05
React 中 useDeferredValue 和 startTransition 的核心区别与使用场景
React 中 和 ,两者都用于优化性能,但适用场景和实现方式不同。 核心概念解析 1\. useDeferredValue:延迟值更新 作用 :告诉 React 延迟更新某个值 ,直到所有高优先级渲染任务完成。 适用场景 :当某个值的更新会触发计算密集型渲染(如复杂列表、图表),但需要优先保证其他高优先级 UI(如输入框)的响应。 机制 : 保持旧值显示,后台计算新值。 计算完成后,用新值更新 UI,期间允许用户继续交互。 示例 :...
学习
react2024-12-18
Antd 样式覆盖
&nbsp; 目前作者所在的业务正在升级 Antd5.0,不得不说 Antd5.0 真的太香了,但是由于团队的 UE 规范进行了大改版,Antd5.0 所有的 必须对齐最新的规范,因此就需要对 Antd5.0 组件做主题定制。 现状 目前针对项目中使用的 antd 高频组件进行了梳理,并且根据规范基于 design token 对这些高频组件进行样式定制,对于主题色、圆角、边框、字体等用户 高的主题都已经满足团队规范。 虽然 Desig...
学习
react2024-12-06
React 19 终于发布,一大波新功能正式升级
这篇文章主要介绍了 React 19 的新功能,包括 Actions 自动处理数据突变相关状态,新增的 useActionState、useFormStatus、useOptimistic 等 hook,新的 use API 读取渲染资源,新的 React DOM 静态 API 生成静态站点,以及 React 服务器组件中的 RSC 和 RSA 等内容。同时提到后续会分享针对旧版功能的优化和改良。 关联问题: React 19 性能如何...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录