こんにちは。チームのコードレビューをしていて、「またPropsのバケツリレーが発生しているな……」とか、「この汎用モーダル、中に渡すコンテンツが変わるたびに新しいPropが増えてカオスになってるぞ」と頭を抱えた経験、君たちにもないかい?
中級から一段上のシニアへとステップアップしようとしている君たちなら、コンポーネントの再利用性と柔軟性のジレンマに直面したことが一度や二度ではないはずだ。
今日は、その泥沼から鮮やかに抜け出すための強力な武器、「Slot(スロット)パターン」について話をしよう。Reactの標準機能である`ReactNode`を巧みに操り、コンポーネントの構造を美しく保つための実践的なアプローチを、私の現場での失敗談も交えながら伝授するよ。
—
なぜ「Propsの肥大化」は悪なのか?
よくある悪手から見ていこう。例えば、ヘッダーやモーダル、カードといった「枠組み(レイアウト)」を提供するコンポーネントを作るとする。最初はこうだ。
// 良くあるアンチパターン:何でもかんでもPropで受け取ろうとする例
type BadCardProps = {
title: string;
actionText?: string;
onActionClick?: () => void;
showIcon?: boolean;
iconType?: ‘info’ | ‘warning’;
};
画面の要件が増えるたびに、`actionIcon`が増え、`secondaryAction`が増え、ついには`renderCustomFooter?: () => React.ReactNode`なんていう場当たり的な関数の乱れ打ちが始まる。これではコンポーネントの中身が「Propsをさばくための条件分岐の要塞」になってしまい、何をしているのか分からないモンスターコンポーネントの完成だ。
Slotパターンという名の「穴」を空ける思想
ここで発想を転換する。コンポーネント自体にすべてをハードコーディングするのではなく、「ここに何かがスポッとハマる穴(スロット)を用意しておくので、あとは使う側で自由に差し込んでください」という設計にするのだ。
Web標準のWeb Componentsにある `
—
現場で即戦力になるSlotパターンの実装
百聞は一見にしかず。実務でそのまま使える「多機能カードコンポーネント」を例に、きれいなSlotパターンの実装を見ていこう。
ヘッダー、メインコンテンツ、フッター(アクションボタン等)をそれぞれ独立したスロットとして外部から注入できるように設計する。
import React from ‘react’;
// 1. 各スロットのPropsやコンポーネントの型定義を明確にする
type CardSlotProps = {
children: React.ReactNode;
};
// ヘッダー部分のスロット
export const CardHeader: React.FC
// 現場の小ワザ:ここでラップ用のDOMや共通スタイル(パディングなど)を担保する
return
;
};
// メインコンテンツ部分のスロット
export const CardBody: React.FC
return
;
};
// フッター部分のスロット
export const CardFooter: React.FC
return
;
};
// 2. 枠組みを提供するベースコンポーネント
// 複合コンポーネント(Compound Components)のスタイルを取り入れるとさらに美しくなる
type CardProps = {
children: React.ReactNode;
className?: string;
};
export const Card: React.FC
Header: typeof CardHeader;
Body: typeof CardBody;
Footer: typeof CardFooter;
} = ({ children, className = ” }) => {
return (
);
};
// コンポーネントのドット記法(Compound Componentパターン)で名前空間をまとめる
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
使う側のコードを見よ、この美しさを
このカードコンポーネントを呼び出す側(親コンポーネント)のコードを見てほしい。レイアウトの構造と、そこに流し込むコンテンツが完全に分離されているのが一目でわかるはずだ。
import React from ‘react’;
import { Card } from ‘./Card’;
export const UserProfileCard: React.FC = () => {
return (
{/ 自由な構成でSlotに要素を注入する /}
👤 ユーザープロフィール
名前: 山田 太郎
役割: シニア・フロントエンドエンジニア
Reactの沼にハマって早10年。
);
};
どうだい? `showActionButtons={true}` みたいな不格好なフラグPropを量産しなくても、フッターにどんなボタンをいくつ置くのかは、使う側(UserProfileCard)が完全にコントロールできている。これがSlotパターンの真骨頂だ。
—
ブラウザ裏側とReactの型システム:`ReactNode`の本質
ここで、もう少しレイヤーを下げて、Reactが裏側で何をやっているのかをエンジニア目線で抑えておこう。
私たちが型として指定している `React.ReactNode` は、実のところ以下のユニオン型として定義されている。
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
つまり、`ReactNode` は 「Reactがレンダリングできるすべての物質」 の総称だ。文字列であれ、数値であれ、JSXの要素であれ、フラグメント(`<>…>`)であれ、すべてを内包できる。
⚠️ シニアからの警告:`ReactNode` を使う際の注意点
ここで一つ、実務でやりがちなアンチパターンに言及しておこう。
「なんでも入るから便利だぜ!」と、子要素の型をすべて `any` や雑な `React.ReactNode` で受けてしまうと、「コンポーネントが何を受け取るべきかの意図(契約)」がコードから消え失せる。
もし、特定のサブコンポーネント(例えば今回の `Card.Header` など)しか子として受け入れたくない、あるいは強制したい場合は、先ほどのようにCompound Componentsのテクニックを組み合わせるか、`Children.map` などを使って厳密にバリデーションするアプローチが必要になることもある。ただ、過剰なバリデーションはReactの柔軟性を殺す諸刃の剣なので、基本は「型安全なレイアウトの枠組みを提供し、中身は `ReactNode` で自由にしてもらう」というバランス感覚が実務では最も重要だ。
—
まとめ:明日からの開発にどう活かすか
Slotパターンは、単なる「テクニック」ではなく、「責任の分離(Separation of Concerns)」をフロントエンドのUI層で実現するための思想だ。
- レイアウトや外枠の責任:ベースコンポーネント(`Card`など)が持つ。
- コンテンツや振る舞いの責任:注入する側(スロットに差し込む要素)が持つ。
この境界線をクリアに引けるようになると、君が作るコンポーネント群は見違えるほど頑健になり、チームメンバーから「このコンポーネント、めちゃくちゃ使いやすいですね!」と感謝されるようになるはずだ。
さあ、今日の業務のコードレビューで、もし「Propが10個以上あるモンスターコンポーネント」を見つけたら、そっとこのSlotパターンを提案してみてくれ。君のチームのコードベースが、一歩クリーンに前進することを保証しよう。

コメント