Reactコンポーネントに useState が乱立している構造は、レビューアーとして最も警戒すべき設計の兆候のひとつです。状態数の多さ自体が問題ではありませんが、「どこで」「なぜ」「何のために」保持されているのかという状態責務の可視性が低下している構造は、保守性・再利用性・可読性すべての観点でボトルネックになります。

本稿では、useState が過剰に使用されたコードに対して、レビューアーがどのように設計意図を読み取り、どのような構造改善の視点を持つべきかを整理します。

状態が多いこと自体は問題ではない

まず前提として、「useStateの呼び出し数が多い=悪」という単純な図式は成り立ちません。状態の多さはコンポーネントの複雑性に比例するものであり、状態数が少ないことが良設計であるとは限りません。

ただし、以下のような構造であれば、設計分離の視点が不在である可能性があります。

  • UI用の状態とロジック用の状態が useState のみで混在している
  • 状態がどの責務のために存在するかがコメントや命名で明示されていない
  • 表示状態(トグル、表示フラグなど)が画面上に散在している
状態の分類視点

状態は、少なくとも次の観点で分類できるとレビューがしやすくなります。

  • 表示制御(UIに直接影響する)
  • ユーザー入力(フォームやチェック状態)
  • ロジック管理(一時的な計算・フラグ)
  • 外部同期(API・WebSocket・ローカルストレージなど)

コード例:useStateが多すぎる構造

以下は典型的な例です。ボタン表示、モーダル開閉、エラーフラグ、選択値などがすべて同じコンポーネント内で管理されています。

useStateが乱立した構造
export function ProductTable() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [products, setProducts] = useState([]);
  const [selected, setSelected] = useState(null);
  const [showModal, setShowModal] = useState(false);
  const [editing, setEditing] = useState(false);

  const handleClick = () => {
    setShowModal(true);
  };

  return (
    <>
      {error && <ErrorBox message={error} />}
      {isLoading ? <Loading /> : <Table data={products} />}
      <button onClick={handleClick}>編集</button>
      {showModal && <EditModal product={selected} onClose={() => setShowModal(false)} />}
    </>
  );
}
コードレビュー
@Reviewer: 状態管理の責務が画面内に散在しており、UIロジックとデータロジックが同居しています。表示フラグ系(showModal, editing)と、ローディング・エラー・選択値などの状態が同一コンポーネント内で混在しており、責務分離が不明確です。

状態責務を切り出す3つの判断軸

レビュー観点として、以下の3軸を持つと状態過多構造の評価がしやすくなります。

1. 表示責務とデータ責務の分離

たとえば showModalediting のような表示系状態は、UI制御に限定されるため、構造的にはUIコンポーネントに閉じ込めても問題ありません。一方、productsselected はビジネスロジックとの関連があるため、外部コンテキストやCustom Hookへの抽出が適しています。

2. UI状態の局所化

一画面内でトグル表示やボタン状態などを複数管理する場合、それらを個別の小さなUIコンポーネント(ModalControllerやToggleGroupなど)に切り出すことで、状態を局所化できます。

3. Custom Hookによるロジック抽出

たとえば以下のように状態取得と処理関数を useProducts() などのカスタムフックに切り出すことで、UIとロジックの分離が明示できます。

カスタムフックでロジックを分離
export function useProducts() {
  const [products, setProducts] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchProducts = async () => {
    setIsLoading(true);
    try {
      const res = await api.get("/products");
      setProducts(res.data);
    } catch (e) {
      setError(e.message);
    } finally {
      setIsLoading(false);
    }
  };

  return { products, isLoading, error, fetchProducts };
}
構造レビュー
@Reviewer: ロジック系状態(isLoading, error, products)をCustom Hookへ抽出することで、UI構造の責務が明示され、表示制御の状態と処理が分離されました。設計意図の可読性が向上しています。

構造図で見る責務の切り分け

以下は、状態責務の整理前後での構造イメージの変化です。

UML Diagram

このように、構造上の責務を「視覚的に切り出せるか?」が、レビューアーの判断軸になります。

状態の粒度と責務が不明確な構造の見抜き方

useStateの乱立が見られるコードに共通するもう一つの問題は、「状態の粒度が曖昧」であることです。次のようなケースは、責務と状態が一致していない構造としてレビュー対象になります。

  • ひとつのstateに複数の意味を持たせている(例:const [flags, setFlags] = useState({ isOpen: true, isEditing: false })
  • UI表示の制御とデータ処理の結果を、同じ状態に入れている
  • useEffect など副作用処理のトリガーを useState で制御している

こういった状態の意味が交錯している構造は、バグの温床になるだけでなく、他の開発者にとって極めて読みづらいコードになります。

状態粒度が曖昧な構造
const [formState, setFormState] = useState({
  open: false,
  submitting: false,
  error: null,
  fields: { name: "", price: 0 }
});
粒度過多のレビュー
@Reviewer: `formState` に複数の責務(表示制御・処理中フラグ・入力値・エラー)を同居させており、状態変更の意図や更新対象が曖昧です。責務ごとに状態を分離し、操作単位での再レンダリング範囲も明示化した構造が望ましいです。

状態を“まとめる”のではなく“区切る”という発想

Reactに不慣れな実装者ほど、「関連しそうな状態はひとつのuseStateにまとめたほうがいい」と考えがちですが、これはDOM中心のUI設計や、Vueなどのdataベースの管理構造の影響を受けていることがあります。

Reactにおいては状態は分割されている方が再レンダリングの粒度管理がしやすく、責務分離にも適しているため、むしろ細かく切ることが設計的には合理的です。

ただし、切りすぎても可読性が落ちるため、「意味単位」「UI単位」「ロジック単位」の3軸で分類し、それぞれでバランスを取るようレビュー視点を持ちましょう。

状態整理のチェックリスト(レビュー用)

以下はレビュー観点で使えるチェックリストです。状態数が多いと感じた場合はこの観点で構造を読み解いていくと、設計意図の再確認や改善提案がスムーズに行えます。

状態整理のチェックポイント

UI制御状態とデータ状態は独立できるか?

UI表示のための状態(モーダル表示、ボタン押下フラグなど)と、データやロジック処理のための状態(選択中のデータ、サーバーから取得した値など)は構造的に完全に分離できるケースがほとんどです。

下記のような例では、UI状態は親で持たずにUIコンポーネント側で閉じることができます。

UI状態を内部に閉じ込める例
function EditButton({ onSubmit }) {
  const [isOpen, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(true)}>編集</button>
      {isOpen && <EditModal onSubmit={onSubmit} onClose={() => setOpen(false)} />}
    </>
  );
}
UI責務の明示
@Reviewer: 編集フラグ(isOpen)を親コンポーネントで管理する必要がなくなり、UI制御の責務が明確になりました。ロジック状態との混在が解消されています。

UI状態はユーザーとのインタラクションに直接関係する情報だけを持ち、それ以外は親や外部に任せる設計を基本とすることで、useStateの設置場所と意味が明確になります。

「useStateが多すぎる」構造の本質

レビューアーが指摘すべきは「useStateの数」ではなく、「状態と責務の関係性」です。useStateが10個でも、すべての状態に明確な責務と設計理由があるなら問題はありません。逆に、useStateが3つでも、意味が混在していたり命名が曖昧であれば、それは設計ミスとして指摘すべきです。

状態管理の設計は「その状態がどこで使われ、何のために存在しているか