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
コンポーネントはTitle
、Body
、Body
内に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 経由で渡すのではなく、子コンポーネントとして渡す親子関係で構築されるコンポーネントで非常に有効なデザインパターンです。
- 親コンポーネントと子コンポーネント間の凝集度を高め、より直感的なインターフェースを提供でき、柔軟性やパフォーマンスの向上も期待できます。