⚛️

第1回 Compound Pattern | React デザインパターン入門

2023/12/07に公開

React のコンポーネントにはいくつかのパターンがあります.
よくあるパターンは知識として入れておくと、設計の幅が広がるので、これから何回かに分けて学んでいきます.

以下のサイトの React のところにある設計パターンを全5回に分けて学んでいく予定です.
https://www.patterns.dev/react/

記念すべき第一回は、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 コンポーネントのポイント

  1. State を set している. (コンテントを選択)
  2. 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 コンポーネントのポイント

  1. 選択中のコンテントかどうかを比較
  2. 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>
  );
};

使う側のポイント

  1. Trigger, Content に関してはプロパティで持たせているので、インポートは不要.
  2. 選択中のコンテントはどれか?というロジックは内部で行われているので、使う側が意識する必要はない.

メリット

  • コンポーネントを使う際は、内部の state(状態)、ロジックを意識する必要がない
  • 子コンポーネントを同時にインポートする必要はない

デメリット

  • React.Children.map で、ネストした子要素にはアクセスができない

Coumpound Pattern
https://www.patterns.dev/react/compound-pattern

Discussion