こんにちは!Reactの世界へようこそ。チーフアーキテクトの私です。
Reactを触り始めて、「あ、コンポーネントを細かく分けていこう!」と思ったあたりで、こんな壁にぶつかったことはありませんか?
「親から子へ、さらにその孫へと、何重にもProps(プロップス)をバケツリレーしなきゃいけないの……?コードがごちゃごちゃになって泣きそう!」
……大丈夫ですよ、その悩み、誰もが最初に通る「Reactあるある」の関所ですから安心してください。
今日は、その泥臭いバケツリレーをスマートに解決し、まるで魔法のようにコンポーネント同士が連携できる「Compound Components(コンパウンド・コンポーネント)」という、現場でめちゃくちゃ重宝する設計パターンについてお話しします。
むずかしい言葉は一旦置いておいて、身近な例から優しく紐解いていきましょう!
—
1. 身近な例え:自動販売機で考えてみる
突然ですが、街中にある「自動販売機」を思い浮かべてみてください。
あれって、ボタンを押すだけで、下から「ガチャン」と飲み物が出てきますよね。内部の複雑なメカニズム(お金の計算、在庫の管理、冷やす機能など)は、外側の私たちからは見えません。そして、それぞれのパーツ(硬貨投入口、ボタン、取り出し口)が、まるでチームのように連携して動いています。
Reactの「Compound Components」もこれと全く同じです。
ひとつの大きな機能(例えば「タブ切り替え」や「モーダル」)を、複数の小さな部品コンポーネントのチームとして設計します。そして、親が裏側でこっそり状態(データ)を管理し、子たちに共有することで、外側からはとてもシンプルに使えるようにする——これがこのパターンの正体です。
—
2. なぜPropsのバケツリレーはつらいのか?
通常、Reactでは親から子へデータを渡すとき、Propsを使いますよね。
{/ 親 /}
{/ 子 /}
これが「孫」「ひ孫」と深くなっていくと、途中の「使わない子」まで `data` を受け取って下の階層にパスし続けなければなりません。これを「Propsのバケツリレー(あるいはProp Drilling)」と呼びます。コードのメンテンスが地獄のように面倒になる原因です。
これを解決するのが、Reactの `Context(コンテキスト)` という機能です。
親が「これ、チームのみんなで共有してね!」と置いておける「部室のホワイトボード」のようなものだと思ってください。子コンポーネントは、途中の誰を経由しなくても、そのホワイトボードから直接データを読み取ることができます。
—
3. 実践!「タブメニュー」を作ってみよう
百聞は一見に如かず。実際に手を動かして、Compound Componentsの心地よさを体験してみましょう!
今回は、Webサイトでよく見かける「タブメニュー」を作ります。
理想の形はこうです。使う側(親)からすると、どこにどんな状態があるかなんて意識せず、ただ部品を並べるだけで動いてほしいですよね。
{/ 使う側(App)は、ただ並べるだけでいい! /}
美しくないですか? この直感的なデザインを実現するコードを書いていきます。
ソースコード(日本語コメント付き)
エディタを開いて、以下のコードをそのまま貼り付けてみてください。
import React, { createContext, useContext, useState } from ‘react’;
// 1. 子どもたちがアクセスするための「共有スペース(Context)」を作る
const TabsContext = createContext(null);
// 2. 親となるコンポーネント(状態を管理する司令塔)
export function Tabs({ children, defaultIndex = 0 }) {
const [activeIndex, setActiveIndex] = useState(defaultIndex);
// 子どもたちに配りたいデータや関数をまとめる
const value = { activeIndex, setActiveIndex };
return (
);
}
// 3. タブのボタンを並べるラッパー
function TabList({ children }) {
return
;
}
// 4. 個別のタブボタン
function Tab({ index, children }) {
// 親のContextから「今の選択状態」と「変更する関数」をこっそりもらう
const { activeIndex, setActiveIndex } = useContext(TabsContext);
const isSelected = activeIndex === index;
return (
);
}
// 5. タブの中身(パネル)
function TabPanel({ index, children }) {
const { activeIndex } = useContext(TabsContext);
// 自分が選ばれていなかったら表示しない
if (activeIndex !== index) return null;
return (
);
}
// 6. ドット記法で親に子どもたちをくっつける(これがCompound Componentsのおしゃれなところ!)
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
どうやって使うの?
先ほど定義した `Tabs` コンポーネントを、別のコンポーネント(例えば `App.jsx` など)でこう呼び出します。
export default function App() {
return (
りんごはバラ科の落葉高木樹の実です。シャキシャキして美味しい!
みかんは冬の風物詩。ビタミンCがたっぷり含まれています。
);
}
—
4. チーフアーキテクトからのワンポイントアドバイス
ここで、実務で使うときに気をつけておきたい「ちょっとしたコツ」をシェアしますね。
① `useContext` のエラーハンドリングを優しくしよう
もし、うっかり `Tabs.Tab` を `Tabs` の外側(親に囲まれていない場所)で使ってしまったとき、アプリが真っ白になってエラーを吐いてしまいます。そんなときは、Contextを使うカスタムフックの中で優しくガードしてあげましょう。
function useTabsContext() {
const context = useContext(TabsContext);
if (!context) {
throw new Error(‘Tabs系のコンポーネントは、必ず
}
return context;
}
こういう細かい配慮ができるエンジニアは、チームから本当に重宝されます。
② ドット記法(`Tabs.Tab`)のメリット
JavaScriptのオブジェクトの性質を使って、親に子をプロパティとして生やすことで(`Tabs.Tab = Tab`)、コードを見た瞬間に「あ、これはTabsチームの部品なんだな」と一目で分かるようになります。名前空間が綺麗に整理されるので、大人数での開発でも迷子になりません。
—
まとめ
いかがでしたでしょうか?
Compound ComponentsとContextを組み合わせることで、Propsの面倒なバケツリレーから解放され、直感的で美しいコンポーネント設計ができるようになります。
最初は「なんかコードが増えて難しそう……」と感じるかもしれませんが、一度この書き味を覚えると、もう元のバケツリレーには戻れなくなる中毒性があります(笑)。
ご自身のプロジェクトで、まずは「ボタンのグループ」や「アコーディオンUI」あたりから、ぜひこっそり試してみてくださいね。
あなたのReactライフが、より楽しく、快適なものになりますように!

コメント