【React デザインパターン】Compound Components を使って直感的なコンポーネントを実装しよう

Posted: 2022/6/26
React/
Design pattern

Compound Components

Compound Components は、親コンポーネントで全体を囲み、子コンポーネントをユーザーに指定させるパターンです。膨れ上がったデータを Props 経由で渡すのではなく、子コンポーネントとして渡すようにします。

このようにすることで、親コンポーネントと子コンポーネント間の凝集度を高め、より直感的なインターフェースを提供することが可能です。

HTML の例で考えましょう。

<select>
  <option value="react">React</option>
  <option value="vue">Vue</option>
  <option value="angular">Angular</option>
</select>

<select>はセレクトボックスの大枠を表し、<option>はその中で表示されるアイテムを表しています。

HTML では、<option><select>の子として渡すことができます。<select>要素は状態を管理し、子要素として渡された<option>がセレクトボックスのアイテムとして扱われていることが安易に理解できるかと思います。

React でも非常によく似たことができ、親コンポーネントが暗黙の状態(state)を管理し、子コンポーネントをその要素として渡せます。これがまさにCompound Componentsです。

実装例

今回は例として、Cardコンポーネントを実装してみましょう。CardコンポーネントはTitleBodyBody内にItemを配列として受け取り表示できると想定して実装します。

Props を渡して実装するケース

まず Compound Component パターンを使わず、Props を渡していく方法で実装を考えていきます。

Card コンポーネントを Props を通して受け取り、実装します。

import React, { useState } from "react";

interface CardItem {
  id: number;
  text: string;
}

interface CardProps {
  title: string;
  items: CardItem[];
}

export const Card: React.FC<CardProps> = ({ title, items }) => {
  const [isShow, setIsShow] = useState(false);
  const handleClick = () => setIsShow(show => !show);
  return (
    <div>
      <h2>{title}</h2>
      <div>
        <button onClick={handleClick}>Show items</button>
        {isShow && (
          <div>
            {items.map(item => (
              <div key={item.id}>{item.text}</div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

Propsを介してitems titleを受け取り、表示しています。またstateやロジックなども一体となったコンポーネントとなっていますね。

このCardコンポーネントを利用するコンポーネントを実装してみましょう。

import type { NextPage } from "next";
import { Card } from "../components/Card";

const Home: NextPage = () => {
  const cardItems = [
    { id: 1, text: "親子によるUIとロジックの分離" },
    { id: 2, text: "Propsではなく子コンポーネントとして渡す" },
    { id: 3, text: "コンポーネントの再利用性が上がる" },
  ];
  return (
    <div>
      <Card title="Props pattern" items={cardItems} />
    </div>
  );
};

export default Home;

コードの記述量は少ないですが、使う側ではいまいち直感的なインターフェースになっていないように感じます。またPropsを介して呼び出しているため、Propsに変更があればCardコンポーネント全体の再レンダリングが行われてしまいます。

Compound Component パターンを利用するケース

次に Compound Component パターンを利用するケースについて考えていきましょう。

同様にCardコンポーネントから考えましょう。

import { useState } from "react";

interface CardProps {
  children: React.ReactNode;
}

interface CardComposition {
  Title: React.FC<CardTitleProps>;
  Body: React.FC<CardBodyProps>;
  Item: React.FC<CardItemProps>;
}

interface CardTitleProps {
  children: React.ReactNode;
}

interface CardBodyProps {
  children: React.ReactNode;
}

interface CardItemProps {
  children: React.ReactNode;
}

export const Card: React.FC<CardProps> & CardComposition = ({ children }) => {
  return <div>{children}</div>;
};

const CardTitle: React.FC<CardTitleProps> = ({ children }) => {
  return <h2>{children}</h2>;
};

const CardBody: React.FC<CardBodyProps> = ({ children }) => {
  const [isShow, setIsShow] = useState(false);
  const handleClick = () => setIsShow(show => !show);
  return (
    <div>
      <button onClick={handleClick}>Show items</button>
      {isShow && <div>{children}</div>}
    </div>
  );
};

const CardItem: React.FC<CardItemProps> = ({ children }) => {
  return <div>{children}</div>;
};

Card.Title = CardTitle;
Card.Body = CardBody;
Card.Item = CardItem;

型宣言のためコードが少し増えていますが、Card の Title, Body, Item と責務をそれぞれわけることができています。

また state を保持するステートフルなコンポーネントと UI の表示のみのステートレスなコンポーネントに分割できてもいて、見通しがよくなっています。

次にCardコンポーネントを利用するコンポーネントを実装してみましょう。

import type { NextPage } from "next";
import { Card } from "./components/Card";

const Compound: React.FC = () => {
  const cardItems = [
    { id: 1, text: "親子によるUIとロジックの分離" },
    { id: 2, text: "Propsではなく子コンポーネントとして渡す" },
    { id: 3, text: "コンポーネントの再利用性が上がる" },
  ];
  return (
    <div>
      <Card>
        <Card.Title>Component compound pattern</Card.Title>
        <Card.Body>
          {cardItems.map((cardItem) => (
            <Card.Item key={cardItem.id}>{cardItem.text}</Card.Item>
          ))}
        </Card.Body>
      </Card>
    </div>
  );
};

Props を渡す場合との大きな違いとして、

  • 子コンポーネントとして渡すため、再レンダリング時のコストが下がる
  • 使う側のコンポーネントの柔軟性が上がる
  • より直感的なインターフェースである

などが挙げられます。

まとめ

Compound Componentsは、データを Props 経由で渡すのではなく、子コンポーネントとして渡す親子関係で構築されるコンポーネントで非常に有効なデザインパターンです。

  • 親コンポーネントと子コンポーネント間の凝集度を高め、より直感的なインターフェースを提供でき、柔軟性やパフォーマンスの向上も期待できます。