「継承」という幻想を捨てよ: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
;
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 }) => (
);
—
3. 非同期の競合を回避する:合成による責任の分離
上級者になればなるほど、`useEffect`の中でのAPI呼び出しと状態更新の競合に頭を悩ませるはずだ。これを防ぐのも「合成」の力だ。
データ取得のロジックと、それを表示するUIコンポーネントを完全に切り離し、「Suspense」や「Error Boundary」というReactの合成機構を活用する。自分で複雑なステートマシンを書くよりも、Reactのアーキテクチャが用意した「合成の枠組み」に乗っかる方が、遥かに安全で堅牢だ。
- 状態管理の責務: `useQuery`のようなフックに委譲する。
- UIの責務: 合成されたコンポーネントが、その状態を受け取って描画する。
- エラー処理: 境界(Boundary)をコンポーネントとして合成し、アプリケーション全体を保護する。
—
4. 結論:アーキテクトが目指すべき地平
「優れたコンポーネントとは何か?」と聞かれたら、私はこう答える。
「それ単体で存在意義を完結させつつ、他のどんなコンポーネントとも干渉せずに組み合わさることができるもの」だと。
`props`を追いかけ、ドキュメントを読み漁らなければ理解できない複雑なコンポーネントは、技術的負債以外の何物でもない。合成という手法を通じて、コンポーネントを物理的な部品のように扱い、アプリケーションを「組み立て」ていく。
この泥臭い積み重ねこそが、数年後に「このコード、誰が書いたんだ? 非常にメンテナンスしやすいな」と、次の世代のエンジニアに称賛される、真に堅牢なフロントエンドを構築する唯一の道である。
さあ、エディタを開こう。継承の鎖を断ち切り、合成の自由を手に入れるんだ。

コメント