ある日の我が家
ワイ「よっしゃ、商品を表示するためのコンポーネントができたでぇ」
娘「見せて見せて!」
type Props = {
name: string
price: number
}
export const ProductCard: React.FC<Props> = ({ name, price }) => {
return (
<div className="card">
<h3>{name}</h3>
<p>{price}円</p>
</div>
)
}
ワイ「↑こんな感じや」
ワイ「商品名と価格を表示する、シンプルなコンポーネントや!」
娘「へぇ〜」
娘「商品一覧ページとかで、こんなふうに並べて使う感じ?」
export const ProductList: React.FC = () => {
return (
<div className="list">
<ProductCard name="りんご" price={100} />
<ProductCard name="みかん" price={80} />
<ProductCard name="バナナ" price={120} />
</div>
)
}
ワイ「そうそう」
ワイ「ええやろ?」
娘「うん!シンプルでいいと思う!」
しかし別のページでは「説明文」も表示したい
ワイ「あかん!」
ワイ「おすすめ商品一覧ページでは、商品の説明文も表示せなアカンかったわ!」
ワイ「ほな、説明文もpropsに追加せんとな!」
type Props = {
name: string
price: number
+ description?: string
}
export const ProductCard: React.FC<Props> = ({
name,
price,
+ description,
}) => {
return (
<div className="card">
<h3>{name}</h3>
<p>{price}円</p>
+ {description && <p>{description}</p>}
</div>
)
}
ワイ「完璧や!」
ワイ「説明文は必須じゃないから、オプショナルや!」
ワイ「そんで条件に応じて描画してやるんや」
ワイ「これで、おすすめ商品一覧ページでも使えるでぇ!」
娘「うん!」
export const RecommendList: React.FC = () => {
return (
<div className="recommend-list">
<ProductCard
name="りんご"
price={100}
description="甘みが強く、みずみずしい食感が特徴です!"
/>
</div>
)
}
ワイ「これでええな!」
さらに別のページでは「割引率」を表示したい
ワイ「あかん!」
ワイ「セール商品一覧ページでは、割引率を表示せなアカンかったわ!」
娘「じゃあ、またpropsと条件分岐を追加するの?」
ワイ「せや!」
type Props = {
name: string
price: number
description?: string
+ discountRate?: number
}
export const ProductCard: React.FC<Props> = ({
name,
price,
description,
+ discountRate,
}) => {
return (
<div className="card">
<h3>{name}</h3>
<p>{price}円</p>
{description && <p>{description}</p>}
+ {discountRate && (
+ <p className="discount">{discountRate}%OFF!</p>
+ )}
</div>
)
}
ワイ「おお、ええ感じや!」
ワイ「これでセール商品一覧ページでも使えるな!」
export const SaleList: React.FC = () => {
return (
<div className="list">
<ProductCard
name="りんご"
price={100}
discountRate={20}
/>
<ProductCard
name="みかん"
price={80}
discountRate={30}
/>
</div>
)
}
ワイ「↑これでええな!」
ボタンを表示したいページもある
ワイ「あかん!」
ワイ「別のページでは、カートに追加するボタンを表示せなあかん!」
娘「じゃあ、またpropsを追加するの?」
娘「propsがい〜っぱいになってきたけど・・・」
type Props = {
name: string
price: number
description?: string
discountRate?: number
+ showAddToCartButton?: boolean
+ onAddToCart?: ButtonProps["onClick"]
}
export const ProductCard: React.FC<Props> = ({
name,
price,
description,
discountRate,
+ showAddToCartButton,
+ onAddToCart,
}) => {
return (
<div className="card">
<h3>{name}</h3>
<p>{price}円</p>
{description && <p>{description}</p>}
{discountRate && (
<p className="discount">{discountRate}%OFF!</p>
)}
+ {showAddToCartButton && (
+ <Button onClick={onAddToCart}>
+ カートに追加
+ </Button>
+ )}
</div>
)
}
ワイ「おお、ええやん!」
ワイ「これでカート追加ボタンが必要なページでも使えるな!」
export const AnotherList: React.FC = () => {
return (
<div className="list">
<ProductCard
name="りんご"
price={100}
showAddToCartButton
onAddToCart={() => {
// カートに追加する処理
}}
/>
</div>
)
}
ワイ「これでええな!」
ボタンの見た目もカスタマイズしたい
ワイ「あかん!」
ワイ「あるページでは、ボタンのvariant
も指定せなアカンわ」
娘「え・・・まだpropsを追加するの?」
type Props = {
name: string
price: number
description?: string
discountRate?: number
showAddToCartButton?: boolean
onAddToCart?: ButtonProps["onClick"]
+ buttonVariant?: ButtonProps["variant"]
}
export const ProductCard: React.FC<Props> = ({
name,
price,
description,
discountRate,
showAddToCartButton,
onAddToCart,
+ buttonVariant = 'primary',
}) => {
return (
<div className="card">
<h3>{name}</h3>
<p>{price}円</p>
{description && <p>{description}</p>}
{discountRate && (
<p className="discount">{discountRate}%OFF!</p>
)}
{showAddToCartButton && (
<Button
+ variant={buttonVariant}
onClick={onAddToCart}
>
カートに追加
</Button>
)}
</div>
)
}
娘「う〜ん」
娘「パパ、これってButton
のpropsを2つもバケツリレーしてるね」
娘「商品コンポーネントに、ボタンのvariantを渡すなんて、なんか変な感じ・・・」
娘「JSX部分も条件だらけで、パパ以外の人が読む時に大変かも・・・」
ワイ「せやな・・・」
ワイ「こんなコンポーネント、ワイも保守したくないわ・・・」
ワイ「でもさ・・・」
ワイ「ページによって差し込みたいパーツが違うんやもん・・・」
娘「確かに・・・」
ワイ「いろんな表示パターンがあるから、もういっそ」
ワイ「こうしてまうか・・・?」
// 商品コンポーネントは、枠だけ
export const ProductCard: React.FC<PropsWithChildren> = ({
children
}) => {
return (
<div className="card">
{children}
</div>
)
}
娘「なるほどね」
娘「もう、枠だけにしちゃって」
娘「好きなchildren
を渡して使ってください、って感じね」
ワイ「そうや」
娘「でも、基本的な商品情報とか、大体のスタイルは同じだからなぁ」
娘「色んなページで、似たようなchildren
を何度も書くことになりそう」
ワイ「ぐぬぬ・・・」
娘「じゃあ、コンポジションを活用してみようよ」
娘「カスタマイズしたい部分だけReactNodeで渡せばいいんじゃない?」
コンポジションの活用
娘「こうすればいいと思う!」
type Props = {
name: string
price: number
+ headerSlot?: React.ReactNode
+ bodySlot?: React.ReactNode
+ footerSlot?: React.ReactNode
}
export const ProductCard: React.FC<Props> = ({
name,
price,
+ headerSlot,
+ bodySlot,
+ footerSlot,
}) => {
return (
<div className="card">
+ {headerSlot}
<h3>{name}</h3>
<p>{price}円</p>
+ {bodySlot}
+ {footerSlot}
</div>
)
}
ワイ「おお!」
ワイ「なるほど!」
ワイ「カスタマイズしたい部分だけを、ReactNodeで渡せるようにしたんやな!」
娘「うん!」
娘「これなら、基本の商品情報部分とかスタイルは統一できて」
娘「カスタマイズしたい部分だけを自由に変更できるよ!」
娘「例えば、おすすめ商品一覧ページで、説明文だけ追加したい時は・・・」
// おすすめ商品一覧ページの場合
<ProductCard
name="りんご"
price={100}
bodySlot={<RecommendDescription>
甘みが強く、みずみずしい食感が特徴です!
</RecommendDescription>}
/>
娘「セール商品一覧ページで、割引バッジだけ追加したい時は・・・」
// セール商品一覧ページの場合
<ProductCard
name="みかん"
price={80}
headerSlot={<SaleBadge>
期間限定20%OFF
</SaleBadge>}
/>
娘「ショップページで、カートに追加ボタンだけ追加したい時は・・・」
// ショップページの場合
<ProductCard
name="バナナ"
price={120}
footerSlot={<AddToCartButton
variant="secondary"
onClick={() => console.log("clicked!")}
/>}
/>
ワイ「なるほど!」
ワイ「必要な部分だけをカスタマイズできて、シンプルやな!」
ワイ「propsのバケツリレーもなくなっとるし!」
まとめ
- 「このページでは、これも表示したい!」という要望に沿ってpropsを増やしていくと・・・
- 少し表示内容を変えたいだけなのに、propsがどんどん増えていく
- JSXも条件分岐だらけになり、意図が読み取りづらくなる
- 別コンポーネントのpropsまでバケツリレーすることに
- そんな時はコンポジションを活用しよう
- Compositionとは「構成」「組み立て」という意味
- クソデカコンポーネントにpropsをたくさん渡すのではなく、小さなコンポーネントを組み合わせて構成する
- カスタマイズしたい部分はReactNodeで受け取る
- 以下の方法もある
- Render Propsパターン
- コンポーネントをpropsとして渡す(高階コンポーネント)
- 以下の方法もある
- 共通部分は統一しつつ、柔軟な実装が可能になる
娘「↑こういうことだね!」
ワイ「せやな!」
ワイ「娘ちゃんありがとう!」
〜おしまい〜