責務が曖昧なコンポーネントの分離設計
はじめに
Reactにおけるコンポーネント設計で、レビュー時に最も多く指摘されるのが「責務が混在している構造」です。見た目とロジックが一体化していて読みにくい。副作用の処理と描画が同居していてバグの温床になる。そういったケースに日常的に遭遇します。
この記事では、コンポーネントの責務が曖昧なまま肥大化した構造をレビューでどう判断し、どう分離すべきかを整理していきます。
コンポーネントの「責務」とは何か
Reactコンポーネントは見た目(UI)を表現する手段ですが、しばしば次のような役割を同時に持ち始めます。
- ユーザー入力を受け付ける
 - 外部データを取得する
 - ローカルステートを制御する
 - ドメインロジックを処理する
 - 副作用を管理する
 
このように多機能化しやすいため、レビューアーはまず「何の責務を果たす構造か」を読み取る必要があります。以下のような観点で分類できます。
- 表示専用(Presentational)
 - データ取得・管理(Container)
 - 入力・変化の仲介(Controller)
 - 複数機能の調停(Coordinator)
 
役割が複数存在している場合は分離を検討すべき構造です。
責務が混在した例
よくあるのが、表示とロジックが一体化しているケースです。
export const UserList = () => {
  const [users, setUsers] = useState([]);
  const [keyword, setKeyword] = useState('');
  const filtered = users.filter(u => u.name.includes(keyword));
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers);
  }, []);
  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <ul>
        {filtered.map(u => <li key={u.id}>{u.name}</li>)}
      </ul>
    </div>
  );
};
@Reviewer表示・入力・取得・フィルタが混在しています。責務の切り分けが甘く、再利用やテストが難しい構造です。コンテナとプレゼンテーショナルに分離できないか検討してください。
このコードは以下の責務をすべて内包しています。
- 入力UIの制御
 - 検索キーワードによるフィルタ
 - API呼び出し
 - ステート管理
 - 描画ロジック
 
レビュー時にはこのような多責務の構造を分離対象として判断する必要があります。
分離された構造の例
コンテナと表示用を分けるだけで、構造はかなり読みやすくなります。
export const UserListView = ({ users, keyword, onKeywordChange }) => {
  const filtered = users.filter(u => u.name.includes(keyword));
  return (
    <div>
      <input value={keyword} onChange={e => onKeywordChange(e.target.value)} />
      <ul>
        {filtered.map(u => <li key={u.id}>{u.name}</li>)}
      </ul>
    </div>
  );
};export const UserListContainer = () => {
  const [users, setUsers] = useState([]);
  const [keyword, setKeyword] = useState('');
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers);
  }, []);
  return (
    <UserListView
      users={users}
      keyword={keyword}
      onKeywordChange={setKeyword}
    />
  );
};このように分けることで、表示用はテストもしやすく、ロジックも単純に読み取れます。
プレゼンテーショナルとコンテナの区別
Reactでよく言われる「プレゼンテーショナル vs コンテナ」パターンは、責務を分離するための典型です。
プレゼンテーショナル(見た目専門):
- 親から受け取ったpropsだけで動作
 - ステートや副作用を持たない
 - スタイリングに集中する
 
コンテナ(データ制御):
- ステートやuseEffectなどを扱う
 - 非同期処理を含むことが多い
 - プレゼンテーショナルをラップする
 
レビューの際には、コンポーネントがどちらかに偏っているか、混在していないかをチェックします。
責務分離の典型構造
ありがちな責務混在のパターン
レビュー現場でよく見るパターンとして、次のようなものがあります。
- useEffectの中にsetTimeout、API、イベント登録が全部入っている
 - 入力フォームがそのままサーバーAPIと直接接続している
 - Modalコンポーネントが表示・状態・呼び出し側制御のすべてを持っている
 
こうした構造は「小さく見えるが責務が多い」という罠を含んでいます。サイズではなく構造に注目すべきです。
責務分離のレビュー観点まとめ
レビュー時にチェックすべき観点をまとめます。
- コンポーネントが1つの責務に集中しているか
 - 表示と制御、データとUIが適切に分離されているか
 - ユニットテストが分割された責務単位で書ける構造になっているか
 - useEffectや状態管理がコンポーネント設計全体に及ぼしている影響
 
これらの観点で構造を評価することで、「なぜこのコンポーネントは読みづらいのか」という本質に近づくことができます。
