浅谈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+Spring实现跨域问题的完美解决方法

    React+Spring实现跨域问题的完美解决方法

    这篇文章主要介绍了React+Spring实现跨域问题的完美解决方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-08-08
  • React Hooks的深入理解与使用

    React Hooks的深入理解与使用

    这篇文章主要介绍了React Hooks的深入理解与使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • 基于React-Dropzone开发上传组件功能(实例演示)

    基于React-Dropzone开发上传组件功能(实例演示)

    这篇文章主要介绍了基于React-Dropzone开发上传组件,主要讲述的是在React-Flask框架上开发上传组件的技巧,需要的朋友可以参考下
    2021-08-08
  • React Native自定义路由管理的深入理解

    React Native自定义路由管理的深入理解

    路由管理的功能主要指的页面跳转、goBack、带参数跳转等功能,这篇文章主要给大家介绍了关于React Native自定义路由管理的相关资料,需要的朋友可以参考下
    2021-08-08
  • React组件refs的使用详解

    React组件refs的使用详解

    这篇文章主要介绍了React组件refs的使用详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • react中history(push,go,replace)切换路由方法的区别及说明

    react中history(push,go,replace)切换路由方法的区别及说明

    这篇文章主要介绍了react中history(push,go,replace)切换路由方法的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • React竞态条件Race Condition实例详解

    React竞态条件Race Condition实例详解

    这篇文章主要为大家介绍了React竞态条件Race Condition实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • React中合成事件的实现

    React中合成事件的实现

    React合成事件是对浏览器原生事件的封装,提供跨浏览器一致性API,采用事件委托机制提升性能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-09-09
  • 解决React报错Property value does not exist on type HTMLElement

    解决React报错Property value does not exist&n

    这篇文章主要为大家介绍了React报错Property value does not exist on type HTMLElement解决方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • React自定义Hook-useForkRef的具体使用

    React自定义Hook-useForkRef的具体使用

    本文主要介绍了React自定义Hook-useForkRef的具体使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03

最新评论