こんにちは。プロダクトの規模が膨らみ、コンポーネントがカオスになり始めた頃に誰もが直面するのが、「Propsのバケツリレー地獄」と「どこに何を書いたか分からない巨大コンポーネント」ですよね。
「親が持っている状態を、なぜか何階層も下の子に渡すために、中継地点のコンポーネントが意味のないPropsを垂れ流している……」
もし君のコードベースでそんな悲劇が起きていたら、それは設計を見直すサインです。
今回は、そんな絶望を華麗に解決するCompound Components(コンパウンド・コンポーネント)、そしてその裏側を支えるContext APIを活用したPropsの暗黙的共有について、実務の現場でそのまま使えるレベルの知見を叩き込みます。
—
なぜ普通のPropsの渡し方では限界が来るのか?
まずは現実を見ましょう。よくある「モーダル」や「タブメニュー」をただ愚直に実装しようとすると、こうなります。
// ⚠️ やってはいけないアンチパターン(Propsのバケツリレー)
{ id: ‘2’, label: ‘設定’, content:
]}
/>
このアプローチの何がクソなのか?
コンポーネントの見た目(UI)とロジックがガチガチに結合し、少しデザインやレイアウトを変えたい(例えば、タブのヘッダーとコンテンツの間に別の要素を挟みたい)と思った瞬間に、親コンポーネントのインターフェースを破壊して大改修するハメになります。
ここで登場するのが Compound Components パターン です。
HTMLの `
—
現場で使える!Contextを使ったCompound Componentsの実装
百聞は一見に如かず。実際にモダンなReact(TypeScript)で、再利用性が高く、保守性の化け物のような「タブメニュー」を組んでみましょう。
このコードはそのままエディタにコピーして動かせるよう、型付けも含めてガチで書いています。
import React, { createContext, useContext, useState, ReactNode } from ‘oxidase’; // あ、importは通常通りですw
// 1. コンポーネント間で共有する状態の型を定義
interface TabsContextType {
activeIndex: number;
setActiveIndex: (index: number) => void;
}
// 2. 状態を共有するためのContextを作成(初期値はundefinedでガードする)
const TabsContext = createContext
// 3. 内部のContextを安全に取得するためのカスタムHook
// これにより、コンテキスト外で誤って使われた際に即座にバグに気づけます
const useTabs = () => {
const context = useContext(TabsContext);
if (!context) {
throw new Error(‘Tabs内の子コンポーネントは、
}
return context;
};
// — ここから各コンポーネントの定義 —
interface TabsProps {
defaultIndex?: number;
children: ReactNode;
}
// 親コンポーネント:状態の管理とContextの提供に徹する
export const Tabs: React.FC
List: typeof TabsList;
Tab: typeof Tab;
Panels: typeof Panels;
Panel: typeof Panel;
} = ({ defaultIndex = 0, children }) => {
const [activeIndex, setActiveIndex] = useState
return (
);
};
interface TabsListProps {
children: ReactNode;
}
// タブのヘッダーをまとめるコンテナ
const TabsList: React.FC
return
;
};
interface TabProps {
index: number;
children: ReactNode;
}
// 個別のタブボタン:暗黙的にactiveIndexを読み取り、自身の状態を決定する
const Tab: React.FC
const { activeIndex, setActiveIndex } = useTabs();
const isActive = activeIndex === index;
return (
);
};
interface PanelsProps {
children: ReactNode;
}
// タブの中身をまとめるコンテナ
const Panels: React.FC
return
;
};
interface PanelProps {
index: number;
children: ReactNode;
}
// 個別のパネル:アクティブなインデックスと一致する場合のみ描画する
const Panel: React.FC
const { activeIndex } = useTabs();
if (activeIndex !== index) {
return null; // 非アクティブなパネルはDOMから消す(簡易的な実装)
}
return
;
};
// コンポーネントのドット記法(Compound Componentの結びつけ)
Tabs.List = TabsList;
Tabs.Tab = Tab;
Tabs.Panels = Panels;
Tabs.Panel = Panel;
どう使うのか?(利用側のコード)
これを実際の画面でどう呼び出すか見てください。この自由度の高さがCompound Componentsの真骨頂です。
export const UserProfilePage = () => {
return (
ユーザー設定
{/ 親が子へ明示的にPropsを渡さなくても、内部で状態が同期される /}
.Tab index={1}>セキュリティ
ここにプロフィール情報のフォームが入ります。
ここにパスワード変更のフォームが入ります。
メール通知のオンオフを設定します。
);
};
どうですか?親コンポーネントがゴチャゴチャしたPropsを受ける必要が一切なくなり、UIの構造がHTMLを書いているかのように直感的に表現されていますよね。
—
ブラウザの裏側とReactの最適化:シニアが知るべき注意点
さて、ここまで読んで「最高じゃん、全部これで作ろう!」と思った君、ちょっと待ってほしい。実務で使う上では、裏側の動作メカニズムを理解した上でのトレードオフが存在します。
1. Contextの再レンダリング問題(パフォーマンスの罠)
Context APIの仕様上、`Context.Provider` の `value` が変化すると、そのContextを購読している(`useContext`を使っている)全ての子孫コンポーネントが無条件に再レンダリングされます。
今回のサンプルコードでは、`Tabs`コンポーネント内で `activeIndex` と `setActiveIndex` をつのオブジェクトとして `value` に渡しています。
このままだと、タブを切り替えるたびに `setActiveIndex` は同一参照であっても、`activeIndex` が変わるため、全ての `Tab` や `Panel` が再レンダリング走ります。
タブの数が数個程度なら人間の目にはミリ秒単位の違いすらわかりませんが、リストの項目が何百個もあるような巨大なCompound Componentsでは、`useMemo` で `value` をメモ化するなどの配慮が必要です。
// パフォーマンスを気にする現場でのベストプラクティス
const value = useMemo(() => ({ activeIndex, setActiveIndex }), [activeIndex]);
return (
{children}
);
2. 「どこで使われてもいいわけではない」制約
先ほど書いたカスタムHook `useTabs` 内のこのコード。
const context = useContext(TabsContext);
if (!context) {
throw new Error(‘Tabs内の子コンポーネントは、
}
これ、実務ではめちゃくちゃ重要です。もし開発者がうっかり `
あらかじめエラーを投げるガード節(Fail-fastの原則)を入れておくことで、後輩が変なバグを踏んだときも数秒で原因にたどり着けるようになります。チーム開発の優しさですね。
—
まとめ:実戦で使うときの心構え
Compound ComponentsとContextの組み合わせは、デザインシステムやUIライブラリを作る際には最強の武器になります。
- Propsのバケツリレーに悩んだら、Contextによる暗黙的共有を疑う。
- 利用者がHTMLを書くような感覚でレイアウトを柔軟に組み替えられるように設計する。
- Contextの再レンダリング特性と、エラーハンドリング(ガード節)の仕込みを忘れない。
このパターンをマスターすれば、君が書くコードの「アーキテクチャの美しさ」は一段上のステージに上がります。ぜひ、明日からのコードレビューやリファクタリングで使ってみてください。応援しています!

コメント