基于 React Universe Conf 2025 中 Fernando Rojo 的演讲《Composition is all you need》,以下是关于如何使用**组合模式(Composition Pattern)**重构复杂 React 组件的教程和代码总结。
React 组件的组合模式之道 (Composition Pattern)
1. 核心问题:单体组件的陷阱 (The Monolith Trap)
在开发初期,我们通常创建一个简单的组件(如 Composer 输入框)。随着需求增加(支持多态、编辑模式、转发模式等),我们往往通过添加 Boolean 属性来控制功能。
反模式代码示例:
jsx
// ❌ 典型的"单体"组件,充满条件判断
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 共享状态。
基础架构代码
jsx
// 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)
jsx
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)
需求差异:
- 不需要拖拽上传 (
DropZone)。 - 底部按钮不同(取消/保存)。
- 某些操作按钮不可见(如附件)。
组合实现: 我们只需要不渲染 不需要的组件,并替换 底部的按钮即可,无需任何 Boolean 属性。
jsx
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)
复杂点:
- 这是一个模态框(Modal)。
- 提交按钮在 Composer 外部(Modal 的 Footer)。
- 状态是临时的(Ephemeral),不需要同步到服务器。
代码实现:
jsx
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>;
}
总结:为什么要这样做?
- 消除"布尔地狱": 不再需要传递
isEditing={true}、isForwarding={true}并在组件深处做判断。需要什么功能,就渲染什么组件。 - 状态灵活性: 同一套 UI 组件可以配合
useState(本地)、Redux/Zustand(全局)甚至Ref一起工作,只要通过 Provider 传入即可。 - 可维护性: 当需要在"转发"功能中修改按钮样式时,你只需要修改
ForwardMessageDialog,完全不会影响到"频道聊天"的代码。 - AI 友好: 这种结构化、声明式的代码更容易被 AI 理解和生成,减少了 AI 产生幻觉(Hallucination)或逻辑错误的概率。
一句话总结: 不要把所有的逻辑塞进一个组件里。提升你的状态 (Lift your state),组合你的内部组件 (Compose your internals)。