淺談React 組件的組合模式之道(Composition Pattern)
基于 React Universe Conf 2025 中 Fernando Rojo 的演講《Composition is all you need》,以下是關于如何使用**組合模式(Composition Pattern)**重構復雜 React 組件的教程和代碼總結。
React 組件的組合模式之道 (Composition Pattern)
1. 核心問題:單體組件的陷阱 (The Monolith Trap)
在開發(fā)初期,我們通常創(chuàng)建一個簡單的組件(如 Composer 輸入框)。隨著需求增加(支持多態(tài)、編輯模式、轉發(fā)模式等),我們往往通過添加 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)泛濫,且容易出現不可能的狀態(tài)組合。
2. 解決方案:組合模式 (Composition)
與其通過屬性(Props)告訴組件做什么,不如通過子組件(Children)直接構建組件。這種方式類似于 Radix UI 的設計理念。
我們將大組件拆分為多個小的、職責單一的子組件,并通過 Context 共享狀態(tài)。
基礎架構代碼
// 1. 創(chuàng)建 Context
const ComposerContext = createContext(null);
// 2. Provider 組件:管理狀態(tài)和對外接口
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. 實戰(zhàn)重構:構建不同的 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)
需求差異:
- 不需要拖拽上傳 (
DropZone)。 - 底部按鈕不同(取消/保存)。
- 某些操作按鈕不可見(如附件)。
組合實現: 我們只需要不渲染不需要的組件,并替換底部的按鈕即可,無需任何 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. 進階技巧:狀態(tài)提升與解耦 (Lift Your State)
這是該演講最核心的觀點。狀態(tài)管理應該與 UI 組件解耦。
Composer 的 UI 組件(Input, Footer 等)不應該知道狀態(tài)是來自于 useState(本地狀態(tài))還是 useGlobalStore(全局同步狀態(tài))。它們只負責渲染 Provider 提供的數據。
場景 C:轉發(fā)消息 (Forward Message)
復雜點:
- 這是一個模態(tài)框(Modal)。
- 提交按鈕在 Composer 外部(Modal 的 Footer)。
- 狀態(tài)是臨時的(Ephemeral),不需要同步到服務器。
代碼實現:
function ForwardMessageDialog() {
// 1. 狀態(tài)提升:在父組件控制狀態(tài)
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. 將本地狀態(tài)注入 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 外部,但能觸發(fā)提交 */}
<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}并在組件深處做判斷。需要什么功能,就渲染什么組件。 - 狀態(tài)靈活性: 同一套 UI 組件可以配合
useState(本地)、Redux/Zustand(全局)甚至Ref一起工作,只要通過 Provider 傳入即可。 - 可維護性: 當需要在“轉發(fā)”功能中修改按鈕樣式時,你只需要修改
ForwardMessageDialog,完全不會影響到“頻道聊天”的代碼。 - AI 友好: 這種結構化、聲明式的代碼更容易被 AI 理解和生成,減少了 AI 產生幻覺(Hallucination)或邏輯錯誤的概率。
一句話總結: 不要把所有的邏輯塞進一個組件里。提升你的狀態(tài) (Lift your state),組合你的內部組件 (Compose your internals)。
到此這篇關于淺談React 組件的組合模式之道(Composition Pattern)的文章就介紹到這了,更多相關React 組件組合內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
React中setTimeout獲取不到最新State值的原因及解決方案
在 React 開發(fā)中,我們常常需要在異步操作中訪問組件的 State,然而,由于 React 的閉包機制和異步更新特性,setTimeout?中可能會獲取到過時的 State 值,本文將深入解析這一現象的原因,并提供多種解決方案,需要的朋友可以參考下2025-10-10
vite?+?react?+typescript?環(huán)境搭建小白入門教程
這篇文章主要介紹了vite?+?react?+typescript?環(huán)境搭建小白入門教程,本文通過示例圖文相結合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12

