【入門編】 Compound ComponentsにおけるProps共有 – React実践ガイド

こんにちは!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 (

{children}


);
}

// 3. タブのボタンを並べるラッパー
function TabList({ children }) {
return

{children}

;
}

// 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 (

{children}

);
}

// 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ライフが、より楽しく、快適なものになりますように!

コメント

タイトルとURLをコピーしました