40
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社ゆめみAdvent Calendar 2024

Day 10

9歳娘「パパのコンポーネント、条件だらけで大変だね!」

Last updated at Posted at 2024-12-13

ある日の我が家

ワイ「よっしゃ、商品を表示するためのコンポーネントができたでぇ」

娘「見せて見せて!」

ProductCard.tsx
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>
  )
}

ワイ「↑こんな感じや」
ワイ「商品名と価格を表示する、シンプルなコンポーネントや!」

娘「へぇ〜」
娘「商品一覧ページとかで、こんなふうに並べて使う感じ?」

ProductList.tsx
export const ProductList: React.FC = () => {
  return (
    <div className="list">
      <ProductCard name="りんご" price={100} />
      <ProductCard name="みかん" price={80} />
      <ProductCard name="バナナ" price={120} />
    </div>
  )
}

ワイ「そうそう」
ワイ「ええやろ?」

娘「うん!シンプルでいいと思う!」

しかし別のページでは「説明文」も表示したい

ワイ「あかん!」
ワイ「おすすめ商品一覧ページでは、商品の説明文も表示せなアカンかったわ!」
ワイ「ほな、説明文もpropsに追加せんとな!」

ProductCard.tsx
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>
  )
}

ワイ「完璧や!」
ワイ「説明文は必須じゃないから、オプショナルや!」
ワイ「そんで条件に応じて描画してやるんや」
ワイ「これで、おすすめ商品一覧ページでも使えるでぇ!」

娘「うん!」

RecommendList.tsx
export const RecommendList: React.FC = () => {
  return (
    <div className="recommend-list">
      <ProductCard
        name="りんご"
        price={100}
        description="甘みが強く、みずみずしい食感が特徴です!"
      />
    </div>
  )
}

ワイ「これでええな!」

さらに別のページでは「割引率」を表示したい

ワイ「あかん!」
ワイ「セール商品一覧ページでは、割引率を表示せなアカンかったわ!」

娘「じゃあ、またpropsと条件分岐を追加するの?」

ワイ「せや!」

ProductCard.tsx
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>
  )
}

ワイ「おお、ええ感じや!」
ワイ「これでセール商品一覧ページでも使えるな!」

SaleList.tsx
export const SaleList: React.FC = () => {
  return (
    <div className="list">
      <ProductCard
        name="りんご"
        price={100}
        discountRate={20}
      />
      <ProductCard
        name="みかん"
        price={80}
        discountRate={30}
      />
    </div>
  )
}

ワイ「↑これでええな!」

ボタンを表示したいページもある

ワイ「あかん!」
ワイ「別のページでは、カートに追加するボタンを表示せなあかん!」

娘「じゃあ、またpropsを追加するの?」
娘「propsがい〜っぱいになってきたけど・・・」

ProductCard.tsx
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>
  )
}

ワイ「おお、ええやん!」
ワイ「これでカート追加ボタンが必要なページでも使えるな!」

AnotherList.tsx
export const AnotherList: React.FC = () => {
  return (
    <div className="list">
      <ProductCard
        name="りんご"
        price={100}
        showAddToCartButton
        onAddToCart={() => {
          // カートに追加する処理
        }}
      />
    </div>
  )
}

ワイ「これでええな!」

ボタンの見た目もカスタマイズしたい

ワイ「あかん!」
ワイ「あるページでは、ボタンのvariantも指定せなアカンわ」

娘「え・・・まだpropsを追加するの?」

ProductCard.tsx
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で渡せばいいんじゃないかな?って」

コンポジションでやってみる

娘「こうすればいいと思う!」

ProductCard.tsx
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として渡す(高階コンポーネント)
    • 共通部分は統一しつつ、柔軟な実装が可能になる

娘「↑こういうことだね!」

ワイ「せやな!」
ワイ「娘ちゃんありがとう!」

〜おしまい〜

40
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?