ReactでUIを構築する際、「コンポーネントを分けているのに、結局全部親依存」みたいなコードに出会ったことはないだろうか。
このような構造は一見整理されているようで、実は設計ミスをはらんでいる。
本稿では、親子コンポーネント間の密結合と責務の曖昧化について、レビュー時に見るべきポイントを整理する。

密結合とは何を指しているのか?

「密結合」とは、主に以下のような状況を指す。

  • 親の内部状態を子が直接参照・依存している
  • 子が表示以外のロジック責務も併せ持っている
  • propsの構造が変更されたときに子も影響を受ける
  • props名がドメイン知識に引きずられて冗長かつ曖昧
密結合とは

密結合(tight coupling)とは、コンポーネント間の依存度が高く、片方の変更がもう一方に即座に影響を及ぼす設計を指す。テストや再利用が難しく、保守性が低下する。

よくある構造:親のロジックが子に染み出しているケース

親のロジックが子に混入している構造
export const Parent = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => setCount((c) => c + 1);

  return <CounterButton onClick={handleClick} count={count} />;
};

export const CounterButton = ({ onClick, count }: { onClick: () => void; count: number }) => {
  return <button onClick={onClick}>クリック数: {count}</button>;
};
@Reviewer
一見分離されているように見えるが、実際には子が完全に親の都合でしか動作しない構造。汎用性が低く、再利用先でも必ずcountを渡さなければならない。

このコードでは表示・ハンドリングを分けているように見えるが、CounterButton は「数値とイベントのペア」という構造に固定されており、柔軟性がない。

より柔軟な責務分離の例

以下は、表示責務を独立させるための分離構造。

責務を分離した構造
export const Parent = () => {
  const [count, setCount] = useState(0);
  const handleClick = () => setCount((c) => c + 1);

  return (
    <div>
      <Label text={`クリック数: ${count}`} />
      <IncrementButton onClick={handleClick} />
    </div>
  );
};

export const Label = ({ text }: { text: string }) => <span>{text}</span>;
export const IncrementButton = ({ onClick }: { onClick: () => void }) => (
  <button onClick={onClick}>+</button>
);
@Reviewer
表示とイベントが分離されており、それぞれ独立して利用可能。テストやスタイル調整にも対応しやすい構造。

なぜ密結合が問題になるのか?

以下のような影響が生じやすい。

  • テスト困難性:親がないと子がテストできない
  • 再利用性の欠如:汎用コンポーネントとして抽出できない
  • 命名の一貫性崩壊:親のドメイン知識が子に伝播する
  • 責務混在:副作用やロジックが子コンポーネントに含まれてしまう

実例で見る密結合と疎結合の違い

UML Diagram

密結合を見抜くレビュー観点リスト

状態管理と密結合の関係

状態管理ライブラリ(例:Recoil, Zustand, Jotaiなど)を導入すれば、親子のprops伝搬を最小限に抑えられる。
ただし、それは「密結合を外から見えにくくする」だけのケースもあるので、構造そのものが適切であるかをレビューで確認することが重要。

どうしてもpropsが必要な場合の対処策

  • useMemouseCallbackで意図を明確に保つ
  • childrenパターンで自由度を確保
  • props構造をflattenしすぎない(ネスト構造も視野に)

まとめ

密結合な構造は一見わかりやすく見える反面、実務では再利用性・テスト性・保守性すべてで足を引っ張る。
レビュー時には、props構造や表示責務の混在に注意し、「この子コンポーネントは本当に独立して機能しているか?」を常に問い直す視点が必要だ。