你是否曾为创建需要适应不同场景的灵活React组件而苦恼?我发现了一种彻底改变我React开发方式的模式——复合组件(compound components)。
在本文中,我将通过实际可立即应用到项目中的示例,带你了解这个改变游戏规则的模式。
问题:相似组件,不同场景
让我们从一个常见场景开始:你有两个外观相似但存在于完全不同上下文中的UI元素。
想象一个联系人管理应用,你需要:
- 在专用页面上编辑联系人
- 在模态对话框中编辑联系人
这两个界面共享相同的表单字段、保存/取消按钮和标题——但它们的呈现方式不同:
- 页面版:全屏布局,标题和按钮位于页眉
- 模态版:紧凑对话框,有自己独特的样式约束
传统解决方案有两种:
- 创建两个逻辑重复的独立组件
- 创建一个根据上下文进行条件渲染的复杂组件
这两种方案都不理想。让我们看看复合组件如何优雅地解决这个问题。
如果你觉得这有帮助,请点个赞帮助更多人发现这篇指南!
解决方案:复合组件模式
复合组件是一种模式,你创建一个管理状态和行为的父组件,同时暴露用于渲染UI不同部分的子组件。
可以把它想象成HTML中的和——它们协同工作,但可以灵活排列。 下面是我们的实现方式:
// 在EditContactPage.jsx的用法
function EditContactPage() {
return (
<PageLayout>
<EditContact.Root contactId={contactId}>
<div className="right-section">
<EditContact.Title />
<EditContact.SubmitButtons />
</div>
<div className="content">
<EditContact.FormInputs />
</div>
</EditContact.Root>
</PageLayout>
);
}
// 在ContactModal.jsx的用法
function ContactModal({ contactId, onClose }) {
return (
<EditContact.Root contactId={contactId}>
<Modal
onClose={onClose}
title={<EditContact.Title />}
footer={<EditContact.SubmitButtons />}
>
<EditContact.FormInputs />
</Modal>
</EditContact.Root>
);
}
看到这种简洁的设计了吗?两个组件使用相同的构建模块,但通过不同的排列方式来适配各自的特定布局。
构建复合组件
现在让我们看看如何实现这个模式。关键要素包括:
- 用于共享状态的上下文
- 管理逻辑的父组件
- 使用该上下文的子组件
下面是具体实现:
import { createContext, useContext, useState, useEffect } from 'react';
// 创建context
const EditContactContext = createContext(null);
// 创建钩子来用context
function useEditContactContext() {
const context = useContext(EditContactContext);
if (!context) {
throw new Error('EditContact subcomponents must be used within EditContact.Root');
}
return context;
}
// Root 组件来处理state和逻辑
function Root({ contactId, children }) {
const [formState, setFormState] = useState({
name: '',
email: '',
phone: ''
});
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// 获取联系detail
const { data: contact } = useGetContact(contactId);
// 保存联系信息
const saveContact = useSaveContact({
onSuccess: () => {
// 成功状态下处理
},
onError: (err) => {
setError(err.message);
}
});
// 设置原始form数据
useEffect(() => {
if (contact) {
setFormState({
name: contact.name,
email: contact.email,
phone: contact.phone
});
}
}, [contact]);
// 处理提交的表单
const handleSubmit = async () => {
setIsLoading(true);
try {
await saveContact.mutateAsync({
id: contactId,
...formState
});
} finally {
setIsLoading(false);
}
};
// context值
const value = {
contact,
formState,
setFormState,
error,
isLoading,
handleSubmit
};
return (
<EditContactContext.Provider value={value}>
{children}
</EditContactContext.Provider>
);
}
// Title 组件
function Title() {
const { contact } = useEditContactContext();
return <>{contact ? `Edit ${contact.name}` : 'Create Contact'}</>;
}
// Submit buttons component
function SubmitButtons() {
const { handleSubmit, isLoading } = useEditContactContext();
return (
<div className="button-group">
<button
className="primary-button"
onClick={handleSubmit}
disabled={isLoading}
>
{isLoading ? 'Saving...' : 'Save'}
</button>
<button className="secondary-button">Cancel</button>
</div>
);
}
// Form inputs 组件
function FormInputs() {
const { formState, setFormState, error } = useEditContactContext();
const handleChange = (e) => {
const { name, value } = e.target;
setFormState(prev => ({
...prev,
[name]: value
}));
};
return (
<form className="contact-form">
{error && <div className="error-message">{error}</div>}
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
value={formState.name}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={formState.email}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label htmlFor="phone">Phone</label>
<input
id="phone"
name="phone"
value={formState.phone}
onChange={handleChange}
/>
</div>
</form>
);
}
// 导出这些组件
const EditContact = {
Root,
Title,
SubmitButtons,
FormInputs
};
export default EditContact;
为什么这个模式具有革命性?
当我开始使用复合组件后,才真正体会到它的强大之处:
灵活性:你可以将子组件放置在组件树的任何位置
关注点分离:逻辑集中管理,UI组合独立实现
可复用性:相同组件可应用于完全不同的场景
可读性:JSX代码清晰展现UI结构
可维护性:核心逻辑的变更只需在一个地方进行
现代UI库如Chakra UI、shadcn/ui和Radix UI广泛采用这个模式绝非偶然——它能创建既强大又灵活的组件。
何时使用复合组件?
当遇到以下情况时,复合组件特别有用:
- 组件需要在不同上下文中呈现不同布局
- 需要在多个UI元素间共享复杂状态
- 希望开发者能灵活组合你的组件
- 正在构建组件库或设计系统
核心要点:
复合组件让你能创建灵活可组合的UI元素
使用React Context在父子组件间共享状态
该模式实现了逻辑与表现的分离
特别适合需要适应不同布局的场景
下次当你需要构建具有多种变体的复杂组件时,不妨尝试复合组件模式。它可能会像改变我一样,彻底改变你对React组件的认知方式。
哪些React模式改变了你的开发流程?欢迎在评论区分享!