Compound components là 1 design pattern rất hay trong React, 1 kiểu component có thể quản lý internal state của nó và component đó được render như thế nào thì nằm ở phía implementation chứ không phải declaration .
Các component thường sử dụng pattern này có thể kể đến như Tab Group, Tab Panel, Table, Dialog, Modal, …
Giả sử đơn giản mình có 1 component VideoCard, có nhiệm vụ hiển thị title, content, thumbnail
export const VideoCardTitle = ({ title }) => {
return <h2>{title}</h2>;
};
export const VideoCardContent = ({ content }) => {
return <p>{content}</p>;
};
export const VideoCardThumbnail = ({ url, desc }) => {
return <img src={url} alt={desc} />;
};
export const VideoCard = ({ video }) => {
return (
<div>
<VideoCardTitle title={video.title} />
<VideoCardContent content={video.content} />
<VideoCardThumbnail url={video.url} desc={video.description} />
</div>
);
};
Chỉ cần gọi nó và truyền data tương ứng:
// Call API, looping and mapping data to VideoCard component
<VideoCard key={video.id} video={video} />
Việc tách nhỏ các component ra giúp custom style và logic được tách biệt với nhau.
Nhìn qua thì khá bình thường và đoạn code này đảm bảo tuân thủ theo các nguyên tắc:
- Single responsibility principle: Mỗi component có 1 nhiệm vụ riêng biệt
- Interface Segregation Principle: Chỉ nhận các props cần thiết cho riêng từng component con
Và rồi 1 ngày, khách hàng yêu cầu có thêm các tính năng như download, series, parental control, … Từ đó, component này sẽ có thêm nhiều biến thể khác.
Cách xử lý thông thường theo mình thấy là truyền thêm props vào, giả sử:
export const VideoCard = ({
video,
isSeries,
isParentalControl,
downloadProgress,
}) => {
return (
<div>
{isSeries && <Badge title="Series" />}
{!isSeries && <VideoCardTitle title={video.title} />}
{!isSeries && <VideoCardContent content={video.content} />}
{!isParentalControl ? (
<VideoCardThumbnail url={video.url} desc={video.description} />
) : (
<ThumbnailDefault />
)}
{!isSeries && downloadProgress && (
<DownloadProgressBar progress={downloadProgress} />
)}
</div>
);
};
Đoạn code trên đã trở nên rối rắm hơn rồi phải không? Một đống props và conditional/ternary operator. Ngoài ra rất dễ gây ra prop drilling nếu dự án càng scale lớn hơn. Đồng thời, ta cũng vi phạm nguyên tắc Open-Closed Principle (mở cho việc mở rộng, nhưng đóng cho việc sửa đổi) vì đã sửa trực tiếp vào component VideoCard
Và React Compound Pattern sẽ giải quyết vấn đề này như thế nào?
Thay vì gọi thẳng các components con riêng lẻ vào component cha VideoCard, ta dùng syntax như sau:
const VideoCardContext = createContext(undefined);
export const VideoCard = ({ children, video }) => {
return (
<VideoCardContext.Provider value={{ video }}>
<div className="video-card">{children}</div>
</VideoCardContext.Provider>
);
};
VideoCard.Title = VideoCardTitle;
VideoCard.Content = VideoCardContent;
VideoCard.Thumbnail = VideoCardThumbnail;
Đồng thời sử dụng context API để truyền props giữa các component (chỉ cho phép context này sử dụng cho VideoCard).
import { createContext, useContext } from "react";
const useVideoCardContext = () => {
const context = useContext(VideoCardContext);
if (!context) {
throw new Error("useVideoCard must be use within a VideoCard");
}
return context;
}
export const VideoCardTitle = () => {
const title = useVideoCardContext().video.title;
return <h2>{title}</h2>;
};
export const VideoCardContent = () => {
const content = useVideoCardContext().video.content;
return <p>{content}</p>;
};
export const VideoCardThumbnail = () => {
const { url, desc } = useVideoCardContext().video;
return <img src={url} alt={desc} />;
};
Và cuối cùng sử dụng nó như sau:
<VideoCard key={video.id} video={video}>
<VideoCard.Title />
<VideoCard.Content />
<VideoCard.Thumbnail />
</VideoCard>;
Nếu có thêm nhiều biến thể khác, ta chỉ cần tự điều chỉnh children của VideoCard (add/remove/reorder)
// Series Card
<VideoCard key={video.id} video={video}>
<Badge title='Series'>
<VideoCard.Thumbnail />
</VideoCard>;
// Download Video Card
<VideoCard key={video.id} video={video}>
<DownloadProgressBar progress={downloadProgress} />
<VideoCard.Thumbnail />
<VideoCard.Title />
</VideoCard>;
Với cách làm trên, code đã trở nên clean hơn, tăng scalability thoả mãn được các nguyên tắc:
- Single responsibility Principle
- Open-Closed Principle
- Interface Segregation Principle
