Part 2 · コンポーネントと props
合成という考え方
継承ではなく組み合わせで作る理由
コンポーネントを作っていくと、必ずこの分かれ道に来ます。
似ているけれど少し違うものを、どう表現するか。
多くの言語では、継承して差分だけ書き換えます。 React ではそれをしません。組み合わせて作ります。これを合成と呼びます。
props で全部を受け取ろうとすると
カードを作るとします。最初はタイトルと本文だけでした。 そこへ「バッジも出したい」「下に更新日も出したい」と要望が来ます。
props で受け取る作りだと、こうなっていきます。
表示できるものが増えるたびに props が増える
表示したいものが増えるたびに、props も増えていく
最終更新: 今日
問題は 2 つあります。
- props が際限なく増える …
showBadgeとbadgeLabelのように、 出すかどうかと中身がセットで増えていきます - 決めた形しか出せない … 「バッジの代わりにボタンを置きたい」と言われたら、 また新しい props が必要です
そして Card は、あらゆる使われ方を知っている巨大な部品になっていきます。使う人が増えるほど、部品が複雑になる作りです。
props-explosion.tsx
// 中身のパターンを props で受け取ろうとすると、こうなっていく
function Card({
title,
description,
badgeLabel,
showBadge,
showFooter,
footerText,
}: {
title: string;
description: string;
badgeLabel?: string;
showBadge?: boolean;
showFooter?: boolean;
footerText?: string;
}) {
return (
<div className="rounded-md border">
<div className="flex items-center justify-between gap-3 border-b p-3">
<span className="font-semibold">{title}</span>
{showBadge && (
<span className="rounded border px-2 py-0.5 text-xs">
{badgeLabel}
</span>
)}
</div>
<p className="p-3 text-muted-foreground">{description}</p>
{showFooter && (
<p className="border-t p-3 text-muted-foreground">{footerText}</p>
)}
</div>
);
}
export function PropsExplosion() {
return (
<Card
title="メンバー"
description="表示したいものが増えるたびに、props も増えていく"
showBadge
badgeLabel="3 名"
showFooter
footerText="最終更新: 今日"
/>
);
}組み合わせて作る
合成では、逆の発想をします。枠は場所だけを決めて、何を置くかは呼ぶ側が決める。
見た目は同じだが、Card は中身を知らない
Card、CardHeader、CardBody、CardFooter。 どれも children を置くだけの、数行の部品です。showBadge のような props はひとつもありません。
それでいて、できることは増えています。 バッジの代わりにボタンを置きたければ、呼ぶ側でそう書けばいいだけです。Card を直す必要はありません。
composed.tsx
import type { ReactNode } from "react";
// 枠は「並べる場所」だけを決める。中身が何かは知らない
function Card({ children }: { children: ReactNode }) {
return <div className="rounded-md border">{children}</div>;
}
function CardHeader({ children }: { children: ReactNode }) {
return (
<div className="flex items-center justify-between gap-3 border-b p-3">
{children}
</div>
);
}
function CardBody({ children }: { children: ReactNode }) {
return <div className="p-3 text-muted-foreground">{children}</div>;
}
function CardFooter({ children }: { children: ReactNode }) {
return <div className="border-t p-3 text-muted-foreground">{children}</div>;
}
export function Composed() {
return (
<Card>
<CardHeader>
<span className="font-semibold">メンバー</span>
<span className="rounded border px-2 py-0.5 text-xs">3 名</span>
</CardHeader>
<CardBody>必要なものだけを、必要な場所に置く</CardBody>
<CardFooter>最終更新: 今日</CardFooter>
</Card>
);
}特別なものは、包んで作る
「よく使う組み合わせ」が決まっているなら、 それを包んだ部品を作ります。継承ではなく、汎用のものを使う専用のものを作るわけです。
// 汎用の Alert を使って、専用の ErrorAlert を作る
function ErrorAlert({ children }: { children: ReactNode }) {
return <Alert tone="error" icon={<XIcon />}>{children}</Alert>;
}ErrorAlert は Alert を継承しているのではなく、使っているだけです。 呼ぶ側は tone を指定しなくてよくなり、Alert 自体は何も変わりません。
なぜ継承を使わないのか
継承だと、親を直したときにそれを継いだ全部に影響します。 しかも「どこまでが親の機能か」を知らないと、子を読めません。
合成なら、使っているものは書いてあるとおりです。ErrorAlert のコードを見れば、Alert を使っていることが 1 行で分かります。隠れた関係が生まれません。
composed.tsx
import type { ReactNode } from "react";
// 枠は「並べる場所」だけを決める。中身が何かは知らない
function Card({ children }: { children: ReactNode }) {
return <div className="rounded-md border">{children}</div>;
}
function CardHeader({ children }: { children: ReactNode }) {
return (
<div className="flex items-center justify-between gap-3 border-b p-3">
{children}
</div>
);
}
function CardBody({ children }: { children: ReactNode }) {
return <div className="p-3 text-muted-foreground">{children}</div>;
}
function CardFooter({ children }: { children: ReactNode }) {
return <div className="border-t p-3 text-muted-foreground">{children}</div>;
}
export function Composed() {
return (
<Card>
<CardHeader>
<span className="font-semibold">メンバー</span>
<span className="rounded border px-2 py-0.5 text-xs">3 名</span>
</CardHeader>
<CardBody>必要なものだけを、必要な場所に置く</CardBody>
<CardFooter>最終更新: 今日</CardFooter>
</Card>
);
}どこで切り替えるか
最初から合成で作る必要はありません。 props が 2 つ 3 つのうちは、そのほうが簡単です。
次のような兆候が出たら、合成に切り替えどきです。
showXxxのような、出すかどうかを決める props が増えてきた- props の数が増え、どれとどれを一緒に使うのか分かりにくい
- 「この場合だけ、ここに別のものを置きたい」という要望が繰り返し来る
props-explosion.tsx
// 中身のパターンを props で受け取ろうとすると、こうなっていく
function Card({
title,
description,
badgeLabel,
showBadge,
showFooter,
footerText,
}: {
title: string;
description: string;
badgeLabel?: string;
showBadge?: boolean;
showFooter?: boolean;
footerText?: string;
}) {
return (
<div className="rounded-md border">
<div className="flex items-center justify-between gap-3 border-b p-3">
<span className="font-semibold">{title}</span>
{showBadge && (
<span className="rounded border px-2 py-0.5 text-xs">
{badgeLabel}
</span>
)}
</div>
<p className="p-3 text-muted-foreground">{description}</p>
{showFooter && (
<p className="border-t p-3 text-muted-foreground">{footerText}</p>
)}
</div>
);
}
export function PropsExplosion() {
return (
<Card
title="メンバー"
description="表示したいものが増えるたびに、props も増えていく"
showBadge
badgeLabel="3 名"
showFooter
footerText="最終更新: 今日"
/>
);
}理解できたか確かめる
確認クイズ
showBadge / badgeLabel のような props が増えてきた。どうする?
確認クイズ
React で継承を使わないのはなぜ?
composed.tsx
import type { ReactNode } from "react";
// 枠は「並べる場所」だけを決める。中身が何かは知らない
function Card({ children }: { children: ReactNode }) {
return <div className="rounded-md border">{children}</div>;
}
function CardHeader({ children }: { children: ReactNode }) {
return (
<div className="flex items-center justify-between gap-3 border-b p-3">
{children}
</div>
);
}
function CardBody({ children }: { children: ReactNode }) {
return <div className="p-3 text-muted-foreground">{children}</div>;
}
function CardFooter({ children }: { children: ReactNode }) {
return <div className="border-t p-3 text-muted-foreground">{children}</div>;
}
export function Composed() {
return (
<Card>
<CardHeader>
<span className="font-semibold">メンバー</span>
<span className="rounded border px-2 py-0.5 text-xs">3 名</span>
</CardHeader>
<CardBody>必要なものだけを、必要な場所に置く</CardBody>
<CardFooter>最終更新: 今日</CardFooter>
</Card>
);
}この章のまとめ
- React では継承せず、組み合わせて作る
- props で中身のパターンを受け取ると、使い道が増えるほど部品が複雑になる
childrenで差し込めるようにすると、 枠は中身を知らなくて済み、変更が呼ぶ側で完結する- 特別なものは、汎用のものを包んで作る
showXxxのような props が増えてきたら切り替えどき
composed.tsx
import type { ReactNode } from "react";
// 枠は「並べる場所」だけを決める。中身が何かは知らない
function Card({ children }: { children: ReactNode }) {
return <div className="rounded-md border">{children}</div>;
}
function CardHeader({ children }: { children: ReactNode }) {
return (
<div className="flex items-center justify-between gap-3 border-b p-3">
{children}
</div>
);
}
function CardBody({ children }: { children: ReactNode }) {
return <div className="p-3 text-muted-foreground">{children}</div>;
}
function CardFooter({ children }: { children: ReactNode }) {
return <div className="border-t p-3 text-muted-foreground">{children}</div>;
}
export function Composed() {
return (
<Card>
<CardHeader>
<span className="font-semibold">メンバー</span>
<span className="rounded border px-2 py-0.5 text-xs">3 名</span>
</CardHeader>
<CardBody>必要なものだけを、必要な場所に置く</CardBody>
<CardFooter>最終更新: 今日</CardFooter>
</Card>
);
}