⚛️
第1回 Compound Pattern | React デザインパターン入門
React のコンポーネントにはいくつかのパターンがあります.
よくあるパターンは知識として入れておくと、設計の幅が広がるので、これから何回かに分けて学んでいきます.
以下のサイトの React のところにある設計パターンを全5回に分けて学んでいく予定です.
記念すべき第一回は、Coumpound Pattern です!
Compound Pattern の概要
Coumpound には「複合の」という意味があります. その名の通り、Coumpound Pattern は「複数コンポーネント間で状態を共有したい」など、お互いに関係性を持っているコンポーネントを実装する際に便利です.
ex) Dropdown, Tabs, Menu など
Compound Component パターンを使った実装例
今回作成するコンポーネントは、シンプルな Tabs コンポーネント.
Google Chrome で上に出ているやつですね!
あのタブの機能を想像してみると、
- 中身を選択する際のボタン部分
- それに応じた中身の部分
の 2つのコンポーネントが特定できます.
内容を切り替える際には、クリックされているタブの状態を共有する必要があります.
ボタン部分を Trigger
中身の部分を Content
として実装します!
そして、これらのコンポーネント間では、どのコンテントが選択中かという状態を共有する必要があります.
Tabs コンポーネントの root を実装
import {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useState,
} from "react";
const TabsContext = createContext<{
selectedContent: string;
setSelectedContent: Dispatch<SetStateAction<string>>;
}>({
selectedContent: "",
setSelectedContent: () => {},
});
type Props = {
children: ReactNode;
};
export const Tabs = ({ children }: Props) => {
const [selectedContent, setSelectedContent] = useState("");
return (
<TabsContext.Provider value={{ selectedContent, setSelectedContent }}>
{children}
</TabsContext.Provider>
);
};
状態を Trigger と Content で共有する準備が整いました!
Trigger コンポーネントを実装
type TriggerProps = {
children: ReactNode;
value: string;
};
const Trigger = ({ children, value }: TriggerProps) => {
const { setSelectedContent } = useContext(TabsContext);
const selectContent = () => {
setSelectedContent(value);
};
return <button onClick={selectContent}>{children}</button>;
};
Tabs.Trigger = Trigger;
Trigger コンポーネントのポイント
- State を set している. (コンテントを選択)
- Tabs コンポーネントのプロパティとして Trigger コンポーネントを設定してあげることで、<Tabs.Trigger> のように呼び出すことができる!
Content コンポーネントを実装
type ContentProps = {
children: ReactNode;
value: string;
};
const Content = ({ children, value }: ContentProps) => {
const { selectedContent } = useContext(TabsContext);
return value === selectedContent && <div>{children}</div>;
};
Tabs.Trigger = Trigger;
Tabs.Content = Content;
Content コンポーネントのポイント
- 選択中のコンテントかどうかを比較
- Tabs.Content でプロパティとしてコンポーネントを設定
Tabs コンポーネントを使う側
import { Tabs } from "./_components/Tabs/Tabs";
const App = () => {
const contents = [
{ value: "TabA", content: "TabA の中身だよ" },
{ value: "TabB", content: "TabB の中身だよ" },
{ value: "TabC", content: "TabC の中身だよ" },
];
return (
<Tabs>
<Tabs.Trigger value="TabA">TabA</Tabs.Trigger>
<Tabs.Trigger value="TabB">TabB</Tabs.Trigger>
<Tabs.Trigger value="TabC">TabC</Tabs.Trigger>
{contents.map(({ value, content }) => (
<Tabs.Content value={value}>{content}</Tabs.Content>
))}
</Tabs>
);
};
使う側のポイント
- Trigger, Content に関してはプロパティで持たせているので、インポートは不要.
- 選択中のコンテントはどれか?というロジックは内部で行われているので、使う側が意識する必要はない.
メリット
- コンポーネントを使う際は、内部の state(状態)、ロジックを意識する必要がない
- 子コンポーネントを同時にインポートする必要はない
デメリット
- React.Children.map で、ネストした子要素にはアクセスができない
Coumpound Pattern
Discussion