「継承」という幻想を捨てろ:Reactにおけるコンポーネント合成(Composition)の極意
Reactを使い始めてしばらくすると、誰もが一度は「共通機能をまとめたベースコンポーネントを作って、それを継承させたい」という誘惑に駆られる。OOP(オブジェクト指向プログラミング)の経験が長い人ほど、その傾向は顕著だ。だが、断言しよう。Reactの世界において「継承」は悪手だ。
Reactの設計思想の核は、継承ではなく「合成(Composition)」にある。今日は、なぜ合成が最強なのか、そして実務で「あ、これ知ってるのと知らないのでは大違いだわ」と言われるような、スロットパターンを用いた高度なコンポーネント設計術を叩き込む。
—
なぜ「継承」ではなく「合成」なのか?
Reactのコンポーネントは、ブラウザから見れば単なる関数(またはクラス)であり、最終的には仮想DOMを通じて最小限のDOM操作を行うための「指示書」だ。
継承を使ってしまうと、親コンポーネントの仕様変更が子に予期せぬ形で伝播する「密結合」の罠にハマる。一方で、合成は「部品を組み合わせて新しいUIを作る」という、レゴブロックのようなアプローチだ。これにより、コンポーネントは「何をするか」という責務に集中でき、再利用性が劇的に向上する。
—
1. 基本の「き」:`children` による疎結合
まずは基本中の基本、`children` プロパティの活用だ。これを使えば、コンポーネントは「中身が何であるか」を知る必要がなくなる。
// ラッパーとしての役割に徹するコンポーネント
const Card = ({ children, title }: { children: React.ReactNode, title: string }) => {
return (
{title}
);
};
このコードの美しさは、`Card` がボタンを含んでいようが、画像を含んでいようが一切関知しない点にある。これが疎結合の第一歩だ。
—
2. 進化系:スロット(Slot)パターン
中級エンジニアが現場でよく直面するのが、「ヘッダー部分とフッター部分を別々に制御したい」という要求だ。ここで「propsを何個も渡す」という力技を使っていないだろうか?
それなら、スロットパターンを導入しよう。
// 複数の場所を注入できるスロットパターン
const Layout = ({ header, content, footer }: {
header: React.ReactNode;
content: React.ReactNode;
footer: React.ReactNode;
}) => {
return (
);
};
// 使う側:宣言的で非常に読みやすい } } このパターンの何が凄いかというと、「コンポーネントの配置構造」と「注入するロジック」を完全に分離できることだ。 将来的にレイアウトの順序が変わっても、修正は `Layout` コンポーネント内だけで済む。 — 最後に、Reactのライブラリ(Radix UIやHeadless UIなど)でも採用されている「複合コンポーネント」の考え方を紹介する。これは、親が管理する「状態」を、子がコンテキストを通じて共有する手法だ。 // コンテキストを利用した高度な合成 const TabsContext = createContext<{ activeTab: string } | null>(null); const Tabs = ({ children }: { children: React.ReactNode }) => { // 子コンポーネントは親のコンテキストを購読する ; この設計の利点は、利用する側が `Tabs` の中で自由に `TabItem` を配置できる点だ。「Tabsの中にさらに別のラッパーを挟みたい」といったイレギュラーな要件にも、Propsのバケツリレーなしで柔軟に対応できる。 — ここまで技術的な話をしてきたが、最後に現場のリアルな知見を一つ。 「合成しすぎ」にも注意せよ。 Reactの仮想DOMは、コンポーネントが細分化されていても、必要な差分だけを賢く計算してくれる。だから、恐れずにコンポーネントを切り出し、合成してくれ。君たちの書くコードが、もっとクリーンで、もっと愛されるものになることを期待している。 さあ、エディタを開いて、まずはその巨大なコンポーネントを分解してみよう。何か詰まったら、またいつでも聞いてくれ。
const App = () => (
content={
footer={}
/>
);3. さらに先へ:複合コンポーネント(Compound Components)
import React, { createContext, useContext } from ‘react’;
const [activeTab, setActiveTab] = React.useState(‘tab1’);
return (
);
};
const TabItem = ({ id, children }: { id: string, children: React.ReactNode }) => {
const context = useContext(TabsContext);
if (context?.activeTab === id) return
return null;
};シニアからのアドバイス:泥臭い実務の現場で
抽象化のやりすぎは、コードを追うのが困難になる「オーバーエンジニアリング」を招く。最初は単純な `children` を使い、どうしてもスロットが必要になった時に初めて分割する。この「遅延判断」こそが、保守性の高いコードを書く秘訣だ。

コメント