【テクニカル・上級編】 コンポーネントの合成(Composition) – React実践ガイド

「継承」という幻想を捨てよ:Reactにおけるコンポーネント合成(Composition)の極致

Reactを触り始めて数年、「コンポーネントを小さく分けましょう」という教訓は耳にタコができるほど聞かされてきたはずだ。しかし、現場のコードベースを見渡すと、`props`のバケツリレーが深海まで続き、`Context`が過剰に汚染され、何のための抽象化か分からない巨大コンポーネントが鎮座している光景に出くわす。

なぜこうなるのか。答えはシンプルだ。多くのエンジニアが、Reactを「UIの継承」の道具として捉えてしまっているからだ。だが、Reactの真骨頂は「合成(Composition)」にある。今日は、堅牢でスケーラブルなアプリケーションを構築するための、一歩先を行くコンポーネント設計術を語ろう。

—

1. 継承と合成:なぜ「is-a」の関係を捨て、「has-a」を採用すべきか

オブジェクト指向の設計に慣れ親しんだエンジニアほど、`BaseButton`を作り、それを継承した`PrimaryButton`や`IconButton`を作ろうとする。しかし、Reactの仮想DOMツリーにおいて継承は地獄への入り口だ。基底クラスが一つ変更されるだけで、依存関係にあるすべての子コンポーネントが再レンダリングの対象となり、メモリリークや意図しない副作用の温床となる。

Reactにおける合成は、「コンポーネントは部品であると同時に、コンテナでもある」という事実を認めることから始まる。

Slotパターンによる疎結合な設計

propsで文字列や数値だけを渡すのではなく、`children`やカスタムスロット(Render Propsに近い概念)を渡すことで、コンポーネント間の結合度を劇的に下げられる。

// 堅牢なContainerコンポーネント例
// 内部の状態管理とレイアウト責務を分離し、見た目を外部から注入する
const DataFetcher = ({ url, render }: { url: string, render: (data: any) => JSX.Element }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
// 実際にはAbortControllerで競合を防ぐこと
fetch(url).then(res => res.json()).then(setData).finally(() => setLoading(false));
}, [url]);

if (loading) return

Loading…

;
return render(data); // 外部から合成されたUIをレンダリングする
};

—

2. レンダリング負荷とメモリ効率:合成を最適化する「境界線」

コンポーネントを細かく分けることは、一見するとパフォーマンスが良いように思えるが、Reactの再レンダリングの仕組みを理解していないと逆効果だ。不必要なコンポーネントの分割は、仮想DOMの比較処理を肥大化させ、メモリ消費量を増大させる。

合成を利用した「再レンダリングの封じ込め」

以下の例を見てほしい。親の`state`が更新された際、子コンポーネントが巻き添えを食わないように、合成を使って「レンダリングの境界」を定義する。

const Parent = () => {
const [count, setCount] = useState(0);

// 「重い計算やレンダリングをするコンポーネント」をchildrenとして渡す
// こうすることで、Parentのstate更新時、HeavyComponentは再レンダリングされない
return (




);
};

// Container自体がchildrenをpropsとして受け取ることで、
// Parentの再レンダリング時にchildrenは参照が維持され、再描画を回避できる
const Container = ({ children }: { children: React.ReactNode }) => (

{children}

);

—

3. 非同期の競合を回避する:合成による責任の分離

上級者になればなるほど、`useEffect`の中でのAPI呼び出しと状態更新の競合に頭を悩ませるはずだ。これを防ぐのも「合成」の力だ。

データ取得のロジックと、それを表示するUIコンポーネントを完全に切り離し、「Suspense」や「Error Boundary」というReactの合成機構を活用する。自分で複雑なステートマシンを書くよりも、Reactのアーキテクチャが用意した「合成の枠組み」に乗っかる方が、遥かに安全で堅牢だ。

  • 状態管理の責務: `useQuery`のようなフックに委譲する。
  • UIの責務: 合成されたコンポーネントが、その状態を受け取って描画する。
  • エラー処理: 境界(Boundary)をコンポーネントとして合成し、アプリケーション全体を保護する。

—

4. 結論:アーキテクトが目指すべき地平

「優れたコンポーネントとは何か?」と聞かれたら、私はこう答える。
「それ単体で存在意義を完結させつつ、他のどんなコンポーネントとも干渉せずに組み合わさることができるもの」だと。

`props`を追いかけ、ドキュメントを読み漁らなければ理解できない複雑なコンポーネントは、技術的負債以外の何物でもない。合成という手法を通じて、コンポーネントを物理的な部品のように扱い、アプリケーションを「組み立て」ていく。

この泥臭い積み重ねこそが、数年後に「このコード、誰が書いたんだ? 非常にメンテナンスしやすいな」と、次の世代のエンジニアに称賛される、真に堅牢なフロントエンドを構築する唯一の道である。

さあ、エディタを開こう。継承の鎖を断ち切り、合成の自由を手に入れるんだ。

コメント

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