淺談React 組件的組合模式之道(Composition Pattern)
基于 React Universe Conf 2025 中 Fernando Rojo 的演講《Composition is all you need》,以下是關(guān)于如何使用**組合模式(Composition Pattern)**重構(gòu)復(fù)雜 React 組件的教程和代碼總結(jié)。
React 組件的組合模式之道 (Composition Pattern)
1. 核心問題:單體組件的陷阱 (The Monolith Trap)
在開發(fā)初期,我們通常創(chuàng)建一個(gè)簡單的組件(如 Composer 輸入框)。隨著需求增加(支持多態(tài)、編輯模式、轉(zhuǎn)發(fā)模式等),我們往往通過添加 Boolean 屬性來控制功能。
反模式代碼示例:
// ? 典型的“單體”組件,充滿條件判斷
function Composer({
onSubmit,
isThread,
isEditingMessage,
initialText,
onCancel
}) {
return (
<div className="composer">
{/* 只有非編輯模式才支持拖拽 */}
{!isEditingMessage && <DropZone />}
<Header />
<Input defaultValue={initialText} />
{/* 線程模式下的額外選項(xiàng) */}
{isThread && <Checkbox label="Also send to channel" />}
<Footer>
{/* 只有非編輯模式才顯示附件按鈕 */}
{!isEditingMessage && <AttachmentButton />}
{/* 提交按鈕邏輯復(fù)雜 */}
{isEditingMessage ? (
<>
<Button onClick={onCancel}>Cancel</Button>
<Button onClick={onSubmit}>Save</Button>
</>
) : (
<Button onClick={onSubmit}>Send</Button>
)}
</Footer>
</div>
);
}
缺點(diǎn): 代碼難以維護(hù),條件渲染(Ternary Hell)泛濫,且容易出現(xiàn)不可能的狀態(tài)組合。
2. 解決方案:組合模式 (Composition)
與其通過屬性(Props)告訴組件做什么,不如通過子組件(Children)直接構(gòu)建組件。這種方式類似于 Radix UI 的設(shè)計(jì)理念。
我們將大組件拆分為多個(gè)小的、職責(zé)單一的子組件,并通過 Context 共享狀態(tài)。
基礎(chǔ)架構(gòu)代碼
// 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. 子組件:消費(fèi) 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. 實(shí)戰(zhàn)重構(gòu):構(gòu)建不同的 Composer
通過組合,我們可以在不修改內(nèi)部邏輯的情況下,構(gòu)建出完全不同的 UI 變體。
場景 A:基礎(chǔ)頻道輸入框 (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)。 - 底部按鈕不同(取消/保存)。
- 某些操作按鈕不可見(如附件)。
組合實(shí)現(xiàn): 我們只需要不渲染不需要的組件,并替換底部的按鈕即可,無需任何 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. 進(jìn)階技巧:狀態(tài)提升與解耦 (Lift Your State)
這是該演講最核心的觀點(diǎn)。狀態(tài)管理應(yīng)該與 UI 組件解耦。
Composer 的 UI 組件(Input, Footer 等)不應(yīng)該知道狀態(tài)是來自于 useState(本地狀態(tài))還是 useGlobalStore(全局同步狀態(tài))。它們只負(fù)責(zé)渲染 Provider 提供的數(shù)據(jù)。
場景 C:轉(zhuǎn)發(fā)消息 (Forward Message)
復(fù)雜點(diǎn):
- 這是一個(gè)模態(tài)框(Modal)。
- 提交按鈕在 Composer 外部(Modal 的 Footer)。
- 狀態(tài)是臨時(shí)的(Ephemeral),不需要同步到服務(wù)器。
代碼實(shí)現(xiàn):
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. 外部按鈕也可以消費(fèi)同一個(gè) Context */}
<Dialog.Footer>
<CopyLinkButton />
{/* 這個(gè)按鈕在 Composer 外部,但能觸發(fā)提交 */}
<ForwardButton />
</Dialog.Footer>
</Composer.Provider>
</Dialog>
);
}
// 外部按鈕實(shí)現(xiàn)
const ForwardButton = () => {
// 因?yàn)楸话?Composer.Provider 內(nèi),依然可以訪問 context
const { actions } = useContext(ComposerContext);
return <Button onClick={actions.submit}>Forward</Button>;
}
總結(jié):為什么要這樣做?
- 消除“布爾地獄”: 不再需要傳遞
isEditing={true}、isForwarding={true}并在組件深處做判斷。需要什么功能,就渲染什么組件。 - 狀態(tài)靈活性: 同一套 UI 組件可以配合
useState(本地)、Redux/Zustand(全局)甚至Ref一起工作,只要通過 Provider 傳入即可。 - 可維護(hù)性: 當(dāng)需要在“轉(zhuǎn)發(fā)”功能中修改按鈕樣式時(shí),你只需要修改
ForwardMessageDialog,完全不會影響到“頻道聊天”的代碼。 - AI 友好: 這種結(jié)構(gòu)化、聲明式的代碼更容易被 AI 理解和生成,減少了 AI 產(chǎn)生幻覺(Hallucination)或邏輯錯(cuò)誤的概率。
一句話總結(jié): 不要把所有的邏輯塞進(jìn)一個(gè)組件里。提升你的狀態(tài) (Lift your state),組合你的內(nèi)部組件 (Compose your internals)。
到此這篇關(guān)于淺談React 組件的組合模式之道(Composition Pattern)的文章就介紹到這了,更多相關(guān)React 組件組合內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React+Spring實(shí)現(xiàn)跨域問題的完美解決方法
這篇文章主要介紹了React+Spring實(shí)現(xiàn)跨域問題的完美解決方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08
基于React-Dropzone開發(fā)上傳組件功能(實(shí)例演示)
這篇文章主要介紹了基于React-Dropzone開發(fā)上傳組件,主要講述的是在React-Flask框架上開發(fā)上傳組件的技巧,需要的朋友可以參考下2021-08-08
react中history(push,go,replace)切換路由方法的區(qū)別及說明
這篇文章主要介紹了react中history(push,go,replace)切換路由方法的區(qū)別及說明,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
React競態(tài)條件Race Condition實(shí)例詳解
這篇文章主要為大家介紹了React競態(tài)條件Race Condition實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
解決React報(bào)錯(cuò)Property value does not exist&n
這篇文章主要為大家介紹了React報(bào)錯(cuò)Property value does not exist on type HTMLElement解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

