Reactのコンポーネント設計原則 ― 入門から実務で迷わないための指針

はじめに

Reactを書き始めると、まず動くものは作れます。しかしプロジェクトが育つにつれ「このコンポーネント、どこまで責務を持たせるべきか」「propsが増えすぎて扱いづらい」「状態をどこに置けばいいか分からない」といった悩みに必ずぶつかります。

この記事は、React初学者から中級者への橋渡しを目的に、実務で迷わないためのコンポーネント設計原則を体系的に整理します。特定のライブラリやフレームワークに依存しない、普遍的な考え方を中心に扱います。

対象読者

  • Reactの基本文法(JSX、props、state、useState/useEffect)は理解している
  • 動くものは作れるが「設計として正しいのか」に自信がない
  • チーム開発で保守しやすいコンポーネントを書きたい

原則1: 単一責任の原則

1つのコンポーネントは1つのことに責任を持つべきです。特にコンポーネントでは「見た目のレンダリング」と「データの取得・加工」を分けて考えると効果的です。

1つのコンポーネントがデータ取得・整形・表示・イベント処理をすべて抱えている状態は、小さいうちは問題ありませんが、機能が増えると肥大化します。

function UserDashboard() {
  const [users, setUsers] = useState<User[]>([]);
  const [keyword, setKeyword] = useState("");

  useEffect(() => {
    fetch("/api/users").then((r) => r.json()).then(setUsers);
  }, []);

  const filtered = users.filter((u) => u.name.includes(keyword));

  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <ul>{filtered.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
    </div>
  );
}

表示専用のコンポーネントに責務を切り出すと、見通しが良くなります。

function UserList({ users }: { users: User[] }) {
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

原則2: ロジックとビューの分離

かつての Presentational / Container パターンは、現在ではカスタムフックによる「ロジックの切り出し」に発展しています。厳密な二分法は薄れましたが、「ロジックとビューを分離する」という思想は今も有効です。

function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [keyword, setKeyword] = useState("");

  useEffect(() => {
    fetch("/api/users").then((r) => r.json()).then(setUsers);
  }, []);

  const filtered = users.filter((u) => u.name.includes(keyword));
  return { filtered, keyword, setKeyword };
}

function UserDashboard() {
  const { filtered, keyword, setKeyword } = useUsers();
  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <UserList users={filtered} />
    </div>
  );
}

ロジックがフック単位でテスト可能になり、ビューは純粋な描画に専念できます。

原則3: propsは最小限に

propsが多すぎるのは設計見直しのサインです。関連するpropsはオブジェクトにまとめ、バリエーションは boolean フラグではなく Composition(children)で表現します。

// Before
<Card title={t} subtitle={s} imageUrl={i} isActive={a} onClick={c} />
// After
<Card item={cardItem} isActive={a} onClick={c} />
<Dialog>
  <Dialog.Header>確認</Dialog.Header>
  <Dialog.Body>本当に削除しますか?</Dialog.Body>
  <Dialog.Footer>
    <button>キャンセル</button>
    <button>削除</button>
  </Dialog.Footer>
</Dialog>

原則4: 状態はできるだけ近くに置く(Colocation)

状態はそれを使うコンポーネントに近い場所へ置き、共有が必要になった時点で初めて親へ持ち上げます(Lifting State Up)。

  • ローカルで完結する状態 → useState
  • 兄弟間で共有する状態 → 共通の親へリフトアップ
  • アプリ全体で必要な状態(認証・テーマ等) → Context / 状態管理ライブラリ

早すぎるグローバル化は、不要な再レンダリングと結合度の増加を招きます。

原則5: 派生状態はstateにしない

他の値から計算できるものは、stateとして持たずレンダリング時に計算します。

// NG: 二重管理は同期ズレの温床
const [count, setCount] = useState(0);
// OK: 導出する
const count = items.length;

計算コストが高い場合のみ useMemo を検討します。デフォルトは「導出」です。

原則6: 抽象化は後から(Rule of Three)

1箇所でしか使われ方が見えていない段階での汎用化は、読みづらいpropsだらけのコンポーネントを生みます。同じものが3回現れたら共通化を検討する、という経験則が実務で役立ちます。

設計のセルフチェック

  • コンポーネント名で「何をするものか」が一言で言えるか
  • propsの数が過剰でないか(目安7個)
  • 単体でテスト・Storybookに載せられるか
  • 状態を移動しても影響が局所に収まるか

注意点

  • ここで挙げた原則は「絶対のルール」ではなく「判断の指針」です。小さなプロトタイプに全部適用すると過剰設計になります
  • チーム開発では原則そのものより「チーム内で基準を揃えること」が重要です

まとめ

  • 単一責任 ― 1コンポーネント1責務
  • ロジックとビューの分離 ― カスタムフックを活用
  • propsは最小限 ― オブジェクト化と Composition
  • 状態は近くに(Colocation)― 早すぎるグローバル化を避ける
  • 派生状態は持たない ― 計算で導出
  • 抽象化は後から ― Rule of Three

設計原則は暗記するものではなく、「なぜそうするか」を理解して初めて武器になります。まずは目の前のコンポーネントを1つ、単一責任の観点で見直すところから始めてみてください。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください