Compound Component Pattern
Demo Code @ Code Sandbox
適合使用的時機
- 多個元件需要相依在一起才能使用時
- CSS 彼此之間有父層和子層的相依關係
- 都需要在某個 Provider 內才能作用
實作
舉例來說,一個 FlyOut 中會包含 List、Item、Toggle 這三個元件,但 List、Item、Toggle 都需要 Flyout 提供的狀態(相依於 Flyout 的狀態),這時候如果我們使用 Compound Component Pattern,就可以讓開發者只需要 import FlyOut 這個元件:
使用方式
import FlyOut from './FlyOut';
const FlyOutMenu = () => {
return (
<FlyOut>
<FlyOut.Toggle />
<FlyOut.List>
<FlyOut.Item>Edit</FlyOut.Item>
<FlyOut.Item>Delete</FlyOut.Item>
</FlyOut.List>
</FlyOut>
);
};
實作方式
import { FC } from 'react';
import { FlyOutProvider } from './context';
import FlyOutToggle from './FlyOutToggle';
import FlyOutItem from './FlyOutItem';
import FlyOutList from './FlyOutList';
interface SubComponents {
Toggle: typeof FlyOutToggle;
Item: typeof FlyOutItem;
List: typeof FlyOutList;
}
interface Props {
className?: string;
}
const FlyOut: FC<Props> & SubComponents = ({ children }) => {
return (
<FlyOutProvider>
<div className="flyout">{children}</div>
</FlyOutProvider>
);
};
// Compound Component Pattern 的關鍵
// dot notation
FlyOut.Toggle = FlyOutToggle;
FlyOut.List = FlyOutList;
FlyOut.Item = FlyOutItem;
export default FlyOut;
Props and Cons
Props
- 對於開發者來說,可以在多個相關的元件中共享狀態
- 對於使用者來說,不需要明確載入 child component,而是只需要載入主元件後就能使用
Cons
- 如果是使用
React.Children.map
的方式,只有 direct children 會取得那些 props,更 nested 在內的 props 就無法取得到父層傳下來的 props - 使用
React.cloneElement
時會做的是 shallow merge,當 props 的名稱有衝突時,props 的值可能會被覆蓋
其他: React.Children
除了使用 React Context 在多個元件間共用狀態外,也可以使用 React.Children 提供的 React.Children.map
搭配 React.cloneElement
方法:
export function FlyOut({ children }: { children: ReactNode }) {
const [open, toggle] = React.useState(false);
return (
<div>
{React.Children.map(children, child =>
React.cloneElement(child, { open, toggle })
)}
</div>
);
}
實際使用此 pattern 的 UI Library
Semantic UI React
- semantic UI React @ Doc
- Dropdown Modules @ Github Source Code
Ant Design
- Menu Component @ Github Source Code
- List Component @ Github Source Code
Reference
- Compound Pattern @ Patters.dev
- React.Children @ reactjs
- cloneElement @ reactjs