浅谈React 组件的组合模式之道(Composition Pattern)

 更新时间:2025年11月30日 16:17:12   作者:iMonster  
本文主要介绍了React组件的组合模式之道(Composition Pattern),通过将复杂组件拆分为多个小、职责单一的子组件,并通过Context共享状态,可以避免单体组件的陷阱,感兴趣的可以了解一下

基于 React Universe Conf 2025 中 Fernando Rojo 的演讲《Composition is all you need》,以下是关于如何使用**组合模式(Composition Pattern)**重构复杂 React 组件的教程和代码总结。

React 组件的组合模式之道 (Composition Pattern)

1. 核心问题:单体组件的陷阱 (The Monolith Trap)

在开发初期,我们通常创建一个简单的组件(如 Composer 输入框)。随着需求增加(支持多态、编辑模式、转发模式等),我们往往通过添加 Boolean 属性来控制功能。

反模式代码示例:

// ❌ 典型的“单体”组件,充满条件判断
function Composer({ 
  onSubmit, 
  isThread, 
  isEditingMessage, 
  initialText, 
  onCancel 
}) {
  return (
    <div className="composer">
      {/* 只有非编辑模式才支持拖拽 */}
      {!isEditingMessage && <DropZone />}
      
      <Header />
      <Input defaultValue={initialText} />
      
      {/* 线程模式下的额外选项 */}
      {isThread && <Checkbox label="Also send to channel" />}
      
      <Footer>
        {/* 只有非编辑模式才显示附件按钮 */}
        {!isEditingMessage && <AttachmentButton />}
        
        {/* 提交按钮逻辑复杂 */}
        {isEditingMessage ? (
           <>
             <Button onClick={onCancel}>Cancel</Button>
             <Button onClick={onSubmit}>Save</Button>
           </>
        ) : (
           <Button onClick={onSubmit}>Send</Button>
        )}
      </Footer>
    </div>
  );
}

缺点: 代码难以维护,条件渲染(Ternary Hell)泛滥,且容易出现不可能的状态组合。

2. 解决方案:组合模式 (Composition)

与其通过属性(Props)告诉组件做什么,不如通过子组件(Children)直接构建组件。这种方式类似于 Radix UI 的设计理念。

我们将大组件拆分为多个小的、职责单一的子组件,并通过 Context 共享状态。

基础架构代码

// 1. 创建 Context
const ComposerContext = createContext(null);

// 2. Provider 组件:管理状态和对外接口
const ComposerProvider = ({ children, state, actions, meta }) => {
  return (
    <ComposerContext.Provider value={{ state, actions, meta }}>
      {children}
    </ComposerContext.Provider>
  );
};

// 3. 子组件:消费 Context
const ComposerInput = () => {
  const { state, actions } = useContext(ComposerContext);
  return (
    <input 
      value={state.text} 
      onChange={(e) => actions.update(e.target.value)} 
    />
  );
};

// ... 其他子组件 (Composer.Header, Composer.Footer, etc.)

3. 实战重构:构建不同的 Composer

通过组合,我们可以在不修改内部逻辑的情况下,构建出完全不同的 UI 变体。

场景 A:基础频道输入框 (Channel Composer)

function ChannelComposer() {
  // 使用自定义 Hook 获取全局频道逻辑
  const { state, actions } = useChannelLogic(); 

  return (
    <Composer.Provider state={state} actions={actions}>
      <Composer.DropZone />
      <Composer.Frame>
        <Composer.Header />
        <Composer.Input />
        <Composer.Footer>
          {/* 使用封装好的通用操作组 */}
          <Composer.CommonActions /> 
          <Composer.SubmitButton />
        </Composer.Footer>
      </Composer.Frame>
    </Composer.Provider>
  );
}

场景 B:编辑消息输入框 (Edit Message Composer)

需求差异:

  1. 不需要拖拽上传 (DropZone)。
  2. 底部按钮不同(取消/保存)。
  3. 某些操作按钮不可见(如附件)。

组合实现: 我们只需要不渲染不需要的组件,并替换底部的按钮即可,无需任何 Boolean 属性。

function EditMessageComposer({ messageId, initialText, onCancel }) {
  const { state, actions } = useEditMessageLogic(messageId, initialText);

  return (
    <Composer.Provider state={state} actions={actions}>
      {/* 移除 DropZone */}
      <Composer.Frame>
        <Composer.Header />
        <Composer.Input />
        <Composer.Footer>
          {/* 手动列出需要的 Action,而不是用通用的 */}
          <Composer.FormatText />
          <Composer.Emoji />
          
          {/* 自定义底部按钮布局 */}
          <div className="flex gap-2">
            <Button onClick={onCancel}>Cancel</Button>
            <Button onClick={actions.submit}>Save</Button>
          </div>
        </Composer.Footer>
      </Composer.Frame>
    </Composer.Provider>
  );
}

4. 进阶技巧:状态提升与解耦 (Lift Your State)

这是该演讲最核心的观点。状态管理应该与 UI 组件解耦。

Composer 的 UI 组件(Input, Footer 等)不应该知道状态是来自于 useState(本地状态)还是 useGlobalStore(全局同步状态)。它们只负责渲染 Provider 提供的数据。

场景 C:转发消息 (Forward Message)

复杂点:

  1. 这是一个模态框(Modal)。
  2. 提交按钮在 Composer 外部(Modal 的 Footer)。
  3. 状态是临时的(Ephemeral),不需要同步到服务器。

代码实现:

function ForwardMessageDialog() {
  // 1. 状态提升:在父组件控制状态
  const [text, setText] = useState("");
  const inputRef = useRef(null);

  // 定义符合 Provider 接口的 state 和 actions
  const state = { text };
  const actions = { 
    update: setText, 
    submit: () => console.log("Forwarding:", text) 
  };

  return (
    <Dialog>
      {/* 2. 将本地状态注入 Provider */}
      <Composer.Provider state={state} actions={actions} meta={{ inputRef }}>
        
        {/* UI 部分 */}
        <Composer.Frame>
          <Composer.Input /> 
          <Composer.Footer>
             {/* 只有少量的操作按钮 */}
             <Composer.Emoji />
          </Composer.Footer>
        </Composer.Frame>

        {/* 3. 外部按钮也可以消费同一个 Context */}
        <Dialog.Footer>
           <CopyLinkButton />
           {/* 这个按钮在 Composer 外部,但能触发提交 */}
           <ForwardButton /> 
        </Dialog.Footer>

      </Composer.Provider>
    </Dialog>
  );
}

// 外部按钮实现
const ForwardButton = () => {
  // 因为被包在 Composer.Provider 内,依然可以访问 context
  const { actions } = useContext(ComposerContext);
  return <Button onClick={actions.submit}>Forward</Button>;
}

总结:为什么要这样做?

  1. 消除“布尔地狱”: 不再需要传递 isEditing={true}isForwarding={true} 并在组件深处做判断。需要什么功能,就渲染什么组件。
  2. 状态灵活性: 同一套 UI 组件可以配合 useState(本地)、Redux/Zustand(全局)甚至 Ref 一起工作,只要通过 Provider 传入即可。
  3. 可维护性: 当需要在“转发”功能中修改按钮样式时,你只需要修改 ForwardMessageDialog,完全不会影响到“频道聊天”的代码。
  4. AI 友好: 这种结构化、声明式的代码更容易被 AI 理解和生成,减少了 AI 产生幻觉(Hallucination)或逻辑错误的概率。

一句话总结: 不要把所有的逻辑塞进一个组件里。提升你的状态 (Lift your state),组合你的内部组件 (Compose your internals)。

到此这篇关于浅谈React 组件的组合模式之道(Composition Pattern)的文章就介绍到这了,更多相关React 组件组合内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • React 在非组件环境切换路由的方法

    React 在非组件环境切换路由的方法

    这篇文章主要介绍了React 在非组件环境切换路由的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-10-10
  • React实现原生APP切换效果

    React实现原生APP切换效果

    最近需要使用 Hybrid 的方式开发一 个 APP,交互和原生 APP 相似并且需要 IM 通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常详细,感兴趣的小伙伴跟着小编一起来看看吧
    2025-01-01
  • React中setTimeout获取不到最新State值的原因及解决方案

    React中setTimeout获取不到最新State值的原因及解决方案

    在 React 开发中,我们常常需要在异步操作中访问组件的 State,然而,由于 React 的闭包机制和异步更新特性,setTimeout 中可能会获取到过时的 State 值,本文将深入解析这一现象的原因,并提供多种解决方案,需要的朋友可以参考下
    2025-10-10
  • react项目自行配置热更新的实现

    react项目自行配置热更新的实现

    本文主要介绍了react项目自行配置热更新的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-11-11
  • vite + react +typescript 环境搭建小白入门教程

    vite + react +typescript 环境搭建小白入门教程

    这篇文章主要介绍了vite + react +typescript 环境搭建小白入门教程,本文通过示例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-12-12
  • React父子组件间的传值的方法

    React父子组件间的传值的方法

    在单页面里面,父子组件传值是比较常见的,这篇文章主要介绍了React父子组件间的传值的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • React中不适当的Hooks使用问题及解决方案

    React中不适当的Hooks使用问题及解决方案

    在 React 开发中,Hooks 提供了一种强大的方式来管理组件的状态和生命周期,然而,不恰当的 Hooks 使用可能会导致组件行为异常、性能问题或难以调试的错误,本文将探讨 React 中常见的不适当 Hooks 使用问题,并提供解决方案,需要的朋友可以参考下
    2025-03-03
  • 详解Immutable及 React 中实践

    详解Immutable及 React 中实践

    Immutable 可以给 React 应用带来数十倍的提升,也有人说 Immutable 的引入是近期 JavaScript 中伟大的发明,因为同期 React 太火,它的光芒被掩盖了。这篇文章主要介绍了Immutable及 React 中的实践,需要的朋友可以参考下
    2018-03-03
  • React使用UI(Ant Design)框架的详细过程

    React使用UI(Ant Design)框架的详细过程

    Ant Design主要用于中后台系统的使用,它提供了丰富的组件和工具,可以帮助开发人员快速构建出美观、易用的界面,同时,Ant Design还提供了详细的文档和示例,方便开发者学习和使用,这篇文章主要介绍了React使用UI(Ant Design)框架,需要的朋友可以参考下
    2023-12-12
  • 一百多行代码实现react拖拽hooks

    一百多行代码实现react拖拽hooks

    这篇文章主要介绍了一百多行代码实现react拖拽hooks,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03

最新评论