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つ、単一責任の観点で見直すところから始めてみてください。
