你是否曾经遇到这样的问题:同一个 React 组件在不同场景下需要呈现完全不同的布局或样式?最近我发现了一种能彻底解决这个问题的组件模式:复合组件(Compound Components)。
本文将通过具体实例,带你了解这一革命性的 React 组件模式,并教你如何立即将它应用到自己的项目中。
📌 场景:组件相似但上下文不同
设想你在做一个通讯录管理应用,你会遇到:
-
在单独的页面编辑联系人信息。
-
在模态框(Modal)中编辑联系人信息。
尽管这两种界面拥有类似的输入框、保存/取消按钮和标题,但布局却完全不同:
-
页面模式:整页布局,标题和按钮位于顶部区域。
-
模态框模式:紧凑布局,需遵循模态框的样式限制。
过去,你可能会选择:
-
创建两个单独的组件,产生大量重复代码。
-
创建一个复杂的组件,根据传入的属性条件渲染。
但以上方式都存在缺陷,代码难以维护且扩展性差。
那么,有没有更好的方式?让我们来看如何用 复合组件模式 优雅地解决它。
🔥 解决方案:复合组件(Compound Components)模式
复合组件模式 是一种组合式的组件设计方法。你会创建一个父组件管理状态和行为,并暴露一系列子组件用于渲染不同的 UI 部分。
你可以将它理解为类似于 HTML 的 <select>
和 <option>
,各个组件共同协作,但具体排列方式可以自由组合。
实际代码使用方式如下:
编辑页面组件示例:
go
// EditContactPage.jsx
function EditContactPage({ contactId }) {
return (
<PageLayout>
<EditContact.Root contactId={contactId}>
<div className="header">
<EditContact.Title />
<EditContact.SubmitButtons />
</div>
<div className="form-container">
<EditContact.FormInputs />
</div>
</EditContact.Root>
</PageLayout>
);
}
模态框组件示例:
go
// ContactModal.jsx
function ContactModal({ contactId, onClose }) {
return (
<Modal
onClose={onClose}
title={<EditContact.Title />}
footer={<EditContact.SubmitButtons />}
>
<EditContact.Root contactId={contactId}>
<EditContact.FormInputs />
</EditContact.Root>
</Modal>
);
}
上面代码清晰地展示了这一模式的优雅之处:
同样的逻辑组件,只需稍微调整布局即可灵活地适应不同场景。
🚀 如何实现复合组件模式?
实现复合组件的核心要素:
-
使用 Context 共享状态。
-
父组件管理逻辑并暴露给子组件。
-
子组件通过 Context 消费共享状态。
具体实现代码:
步骤 1: 创建 Context
go
import { createContext, useContext, useState, useEffect } from 'react';
const EditContactContext = createContext(null);
function useEditContactContext() {
const context = useContext(EditContactContext);
if (!context) {
throw new Error("子组件必须位于 EditContact.Root 内!");
}
return context;
}
步骤 2: 创建父组件 Root 管理状态
go
function Root({ contactId, children }) {
const [formData, setFormData] = useState({
name: '',
email: '',
phone: ''
});
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
// 获取联系人信息
const { data: contact } = useGetContact(contactId);
// 保存联系人信息
const saveContact = useSaveContact({
onSuccess: () => {/*成功处理逻辑*/},
onError: (err) => setError(err.message)
});
useEffect(() => {
if (contact) {
setFormData(contact);
}
}, [contact]);
const handleSubmit = async () => {
setLoading(true);
try {
await saveContact.mutateAsync({ id: contactId, ...formData });
} finally {
setLoading(false);
}
};
const contextValue = {
contact,
formData,
setFormData,
error,
loading,
handleSubmit
};
return (
<EditContactContext.Provider value={contextValue}>
{children}
</EditContactContext.Provider>
);
}
步骤 3: 创建子组件消费 Context
标题组件:
go
function Title() {
const { contact } = useEditContactContext();
return <>{contact ? `编辑 ${contact.name}` : "创建联系人"}</>;
}
提交按钮组件:
go
function SubmitButtons() {
const { handleSubmit, loading } = useEditContactContext();
return (
<div>
<button onClick={handleSubmit} disabled={loading}>
{loading ? '保存中...' : '保存'}
</button>
<button>取消</button>
</div>
);
}
表单输入组件:
go
function FormInputs() {
const { formData, setFormData, error } = useEditContactContext();
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<form>
{error && <div className="error">{error}</div>}
<input name="name" value={formData.name} onChange={handleChange} placeholder="姓名" />
<input name="email" value={formData.email} onChange={handleChange} placeholder="邮箱" />
<input name="phone" value={formData.phone} onChange={handleChange} placeholder="电话" />
</form>
);
}
步骤 4: 导出复合组件
go
const EditContact = {
Root,
Title,
SubmitButtons,
FormInputs
};
export default EditContact;
✨ 为什么这种模式值得推荐?
我在实际使用后,感受到它强大的优势:
-
灵活布局:自由组合子组件,实现不同布局。
-
逻辑复用:核心逻辑统一管理,避免重复。
-
高可读性:JSX 明确展示组件结构,更易理解。
-
维护性强:修改逻辑只需改一处。
-
社区认可:很多流行组件库(如 Chakra UI、Radix UI、shadcn/ui)广泛使用。
🛠️ 何时使用这种模式?
以下场景适合使用复合组件模式:
-
同一组件需适应不同布局。
-
复杂状态需跨多个子组件共享。
-
开发组件库或设计系统,需提供布局组合灵活性。
📌 总结要点
-
复合组件让组件布局更灵活。
-
使用 React Context 管理共享状态。
-
明确的逻辑与布局分离,提升可维护性。
-
尤其适合需适应不同场景布局的组件。
下次再遇到需要实现灵活布局的 React 组件时,不妨尝试一下复合组件模式。或许你也会像我一样,从此彻底改变 React 开发的方式。
最后: