Skip to content
Home
Go back

Compound Components Pattern trong React

Mục lục

Xem mục lục

Compound Components là gì

Compound Components là pattern trong đó nhiều component phối hợp với nhau để tạo thành một UI hoàn chỉnh, chia sẻ state ngầm với nhau mà không cần truyền props tường minh qua từng tầng.

Ý tưởng lấy cảm hứng từ cách các thẻ HTML thuần phối hợp sẵn với nhau, ví dụ <select> và <option>:

<select>
  <option value="a">A</option>
  <option value="b">B</option>
</select>

<select> và <option> phối hợp ngầm với nhau: <select> biết option nào đang được chọn, <option> biết mình thuộc <select> nào, mà không cần truyền tường minh giá trị nào qua lại. Compound Components trong React mô phỏng lại đúng cơ chế phối hợp ngầm này.

Vấn đề của cách tiếp cận truyền thống: props drilling qua nhiều tầng

Cách viết một component Tabs theo kiểu truyền thống thường dồn toàn bộ cấu hình vào một props duy nhất:

<Tabs
  items={[
    { label: "Tab 1", content: <div>Nội dung 1</div> },
    { label: "Tab 2", content: <div>Nội dung 2</div> },
  ]}
/>

Cách này hoạt động, nhưng khó tùy biến từng phần: muốn thêm icon vào một tab cụ thể, muốn đổi thứ tự hiển thị giữa danh sách tab và nội dung, hoặc muốn chèn thêm một phần tử khác (ví dụ nút “Thêm tab mới”) xen giữa, đều phải sửa lại API của Tabs để hỗ trợ thêm props mới. Cấu hình càng phức tạp, props càng phình to và khó kiểm soát.

Triển khai bằng Context

Compound Components dùng Context để component cha chia sẻ state (ví dụ tab nào đang active) cho các component con, không cần truyền props thủ công qua từng tầng:

const TabsContext = createContext(null);

function Tabs({ children, value, defaultTab, onChange }) {
  const [internalTab, setInternalTab] = useState(defaultTab);
  const isControlled = value !== undefined;
  const activeTab = isControlled ? value : internalTab;

  const setActiveTab = id => {
    if (!isControlled) {
      setInternalTab(id);
    }
    onChange?.(id);
  };

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
}

Các component con đọc trực tiếp từ TabsContext thay vì nhận qua props, nên Tabs không cần biết trước cấu trúc bên trong children là gì.

value/onChange theo đúng pattern controlled input của React: không truyền value thì Tabs tự quản lý state (uncontrolled), truyền value thì component cha nắm toàn quyền, Tabs chỉ gọi onChange để báo thay đổi.

Ví dụ: xây dựng component Tabs

function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ id, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      className={activeTab === id ? "active" : ""}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}

function TabPanel({ id, children }) {
  const { activeTab } = useContext(TabsContext);
  if (activeTab !== id) return null;
  return <div className="tab-panel">{children}</div>;
}

Sử dụng:

<Tabs defaultTab="a">
  <Tabs.List>
    <Tabs.Tab id="a">Tab A</Tabs.Tab>
    <Tabs.Tab id="b">Tab B</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel id="a">Nội dung A</Tabs.Panel>
  <Tabs.Panel id="b">Nội dung B</Tabs.Panel>
</Tabs>

Tab và TabPanel không nhận activeTab qua props, mà tự đọc từ TabsContext. Tabs cũng không cần biết trước sẽ có bao nhiêu tab hay chúng được sắp xếp thế nào.

Static Properties, cách gắn component con vào component cha

Để có được cú pháp gọn <Tabs.Tab> thay vì phải import riêng từng component (TabList, Tab, TabPanel), gắn chúng làm static property của Tabs:

Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

Cách này chỉ là cú pháp gọn hơn, không thay đổi bản chất pattern: người dùng Tabs chỉ cần import một component duy nhất, các component con luôn đi kèm sẵn, và namespace Tabs.* giúp rõ ràng chúng thuộc về nhau.

Lợi ích so với props drilling

Đánh đổi cần lưu ý

function useTabsContext() {
  const context = useContext(TabsContext);
  if (!context) {
    throw new Error("Tabs.Tab phải được dùng bên trong Tabs");
  }
  return context;
}

Tổng kết


Share this post:

Previous Post
Container và Presentational Components trong React
Next Post
Tối ưu hiệu năng trong React