useState完全ガイド〜よくある落とし穴と正しい使い方・lazy initialization・関数型update【2026年版】〜

useStateくらい使えるけど、本当に正しく使えているか自信がない」「setStateしたのに値が古いまま反映されない」「オブジェクトのstateを更新したら子コンポーネントが再レンダリングされない」――Reactを書いたことがあれば、誰もが一度はぶつかるuseStateの落とし穴です。本記事は、Reactでもっとも基本でありながらもっとも誤用が多いHookであるuseStateを、現役Reactエンジニア視点で徹底的に解剖します。シグネチャの正確な理解から、lazy initialization(初期値の関数化)、関数型update、オブジェクト・配列・ネスト構造の安全な更新パターン、state構造設計、useReducerやZustand・Jotaiといった代替手段の選び方まで、実用的なコード15本超・比較表・FAQを交えて解説します。読み終えるころには、「とりあえずuseStateで全部やる」段階から脱却し、state設計を意図して選べるレベルまで一気に引き上がるはずです。React 18+のバッチング挙動、StrictModeでの二重実行、React 19・React Compiler時代における立ち位置まで踏まえた、2026年版useState完全ガイドです。

  1. useStateの正確な仕様を再確認する
    1. シグネチャと返り値
    2. 初期値は「最初のレンダリング時にしか評価されない」
    3. lazy initializationで初期化コストをゼロに近づける
  2. setStateの本当の挙動とよくある落とし穴
    1. setStateは「即時反映」ではなく「予約」
    2. 関数型updateで「最新値」を確実に掴む
    3. React 18+のバッチングと「同じ値で更新したらどうなる?」
  3. オブジェクト・配列のstate更新パターン
    1. オブジェクトstateの正しい更新
    2. 配列stateの追加・削除・更新
    3. ネスト構造はImmer・useReducerを検討する
  4. useStateとよく一緒に登場するAPIの関係性
    1. 主要HookとuseStateの役割比較
    2. 「stateにすべきか、refにすべきか」の判断軸
    3. 派生stateはstateにしない
  5. state構造設計の3原則
    1. 原則1: 関連するstateはまとめる
    2. 原則2: 矛盾を防ぐためにstateを分割する
    3. 原則3: stateは可能な限りローカルに置く
  6. useStateの代替を検討すべき5つの局面
    1. 1. アクションが多く・状態遷移が複雑 → useReducer
    2. 2. 複数コンポーネントで共有したい → Context / Zustand / Jotai
    3. 3. サーバーデータ → React Query / SWR / RSC
    4. 4. フォーム → React Hook Form / Conform
    5. 5. URL同期したい → useSearchParams / nuqs
  7. StrictMode・React Compiler・React 19の影響
    1. StrictModeでの二重実行に注意
    2. React Compilerでメモ化の手間が減る
    3. React 19の新API: useオプティミスティック・useフォームステータス
  8. パフォーマンス特性とアンチパターン
    1. 典型的なアンチパターン早見表
    2. 「propsをuseStateにコピーする」問題
    3. 巨大なstateより、分割された複数stateを
  9. useStateを使ったカスタムフック設計
    1. useToggle: 真偽値切り替え
    2. useLocalStorage: localStorageと同期するstate
    3. usePrevious: 前回の値を参照する
  10. キャリア視点:useStateを深く理解すると評価されるポイント
  11. 関連記事:Reactを体系的に深掘りするための地図
  12. FAQ:useStateで現場でよく聞かれる7問
    1. Q1. useStateとuseReducer、どちらを選ぶべき?
    2. Q2. setStateを呼んだ直後にstateを読みたいのですが?
    3. Q3. オブジェクトのstateを毎回スプレッドで書くのが面倒です。
    4. Q4. useStateの値はサーバーサイドレンダリングでどう扱われる?
    5. Q5. 同じ値でsetStateを呼んだら本当に何も起きない?
    6. Q6. useStateとContextを組み合わせれば、Redux/Zustandは要らない?
    7. Q7. useStateの値が「reactiveに」変わったことを別のstateに反映したいのですが?
  13. まとめ:useStateは「Reactの呼吸」

useStateの正確な仕様を再確認する

useStateは「状態を1つ持つだけのシンプルなHook」と説明されがちですが、実際にはReactのレンダリングサイクル・バッチング・Reconciliationと密接に絡む、繊細な仕組みです。まずはAPIの輪郭から正確に押さえます。

シグネチャと返り値

useStateは引数に初期値、または初期値を返す関数を取り、[state, setState]のタプルを返します。setStateはセッターであり、ただの代入関数ではありません。呼び出すたびに、Reactに「次のレンダリングでstateを更新したい」というキューを積みます。

import { useState } from "react";

function Counter() {
  // 第1引数は「初期値」または「初期値を返す関数」
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

このコードは正しく動きますが、後述するように「直前の値に依存する更新」では関数型updateを使うべきです。useStateの戻り値は配列ですが、実態は固定長2のタプル。分割代入の名前は自由なので、複数useStateを並べたときに役割が見えるよう命名するのが定石です。

初期値は「最初のレンダリング時にしか評価されない」

多くの初心者がハマる罠が、これです。コンポーネントが再レンダリングされても、useState(initialValue)のinitialValueは最初の1回しか使われません。にもかかわらず、毎回その式は実行されます。

function Editor() {
  // 重い処理が「毎回実行」される(結果は捨てられる)
  const [doc, setDoc] = useState(parseHugeJSON(localStorage.getItem("doc")));
  return <textarea value={doc} onChange={(e) => setDoc(e.target.value)} />;
}

このコードではparseHugeJSON(...)が毎レンダリング呼ばれ、初回以外の結果は破棄されます。レンダリングのたびに重い処理が走り、UIが詰まる原因になります。これを防ぐのがlazy initialization(遅延初期化)です。

lazy initializationで初期化コストをゼロに近づける

useStateは引数に「値を返す関数」を渡せます。この関数は初回レンダリングでのみ実行されるため、重い計算やlocalStorage読み込みを安全に書けます。

function Editor() {
  // 関数を渡せば、初回レンダリング時にしか実行されない
  const [doc, setDoc] = useState(() =>
    parseHugeJSON(localStorage.getItem("doc") ?? "{}")
  );
  return <textarea value={doc} onChange={(e) => setDoc(e.target.value)} />;
}

たった「()=>」を足すだけで、レンダリングごとに走っていた重い処理が初回1回に削減されます。初期値の取得コストが0でない場合は、原則lazy initializationを使うと覚えておいて損はありません。

setStateの本当の挙動とよくある落とし穴

useStateで一番事故が起きるのが、setStateの呼び出し方です。「setStateは非同期」「直前の値が読めない」「同じ値で再レンダリングが走る」など、現場で頻発する誤解を一つずつ解きほぐします。

setStateは「即時反映」ではなく「予約」

setStateを呼んでも、その直後のcount変数は古いままです。Reactは現在のレンダリングのスナップショットを尊重するため、同じレンダリングサイクル内でstateは変わりません。

function Counter() {
  const [count, setCount] = useState(0);
  const handle = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // 期待: +3 / 実際: +1 (count はクロージャ内で 0 のまま)
  };
  return <button onClick={handle}>{count}</button>;
}

これは「クロージャに閉じ込められた古いstate」を3回足しているため、結果は最後のsetCount(0+1)勝ちで+1にしかなりません。直前の値に依存するときは、必ず関数型updateを使います。

関数型updateで「最新値」を確実に掴む

setStateには「前のstateを受け取って新しいstateを返す関数」を渡せます。これがいわゆる関数型update(updater function)です。

const handle = () => {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
  // 結果: +3 (キューが順番に適用される)
};

関数型updateを使うと、Reactはキューに積まれた各updaterを順番に適用します。以下のような状況では関数型updateが必須です。

  • 連続して同じstateを更新する(カウンタ・チャットの未読数加算など)
  • setTimeout・Promise・イベントハンドラ内など、クロージャに古いstateが閉じ込められる非同期処理
  • カスタムフックの中で「最新値を必ず参照したい」とき
  • useEffectの依存配列にstateを入れたくないとき(updaterだけ呼べばdepsに入れずに済む)

React 18+のバッチングと「同じ値で更新したらどうなる?」

React 18からは、Promise・setTimeout・ネイティブイベントハンドラ内でも自動バッチング(Automatic Batching)が効きます。複数のsetStateは1つのレンダリングにまとめられます。さらに、同じ値(Object.is で等価)で更新した場合、Reactは再レンダリングをスキップします。

const [user, setUser] = useState({ name: "alice" });

// これは再レンダリングが走らない
setUser({ name: "alice" }); // ❌ 違う参照だが、Object.isで比較されるのはuserそのもの

// 正確には: 「次のstate」と「現在のstate」をObject.isで比較
// プリミティブは値、オブジェクトは参照で判定される

プリミティブは「同じ値なら無視」が直感どおりに働きますが、オブジェクト・配列は「新しい参照を渡したら違うとみなされ再レンダリングが走る」のが基本です。後述する「同じ値の代入でレンダリングを止めるテクニック」と合わせて理解しておきましょう。

オブジェクト・配列のstate更新パターン

Reactのstateはimmutable(不変)に扱うのが鉄則です。useStateにオブジェクトや配列を入れた場合、必ず新しい参照を作って渡します。ここを誤ると「stateを変えたのに画面が更新されない」というバグに直結します。

オブジェクトstateの正しい更新

const [user, setUser] = useState({ name: "alice", age: 20 });

// ❌ 直接変更はNG: 参照が変わらず再レンダリングされない
const wrong = () => {
  user.age = 21;
  setUser(user);
};

// ⭕ スプレッドで新しいオブジェクトを作る
const right = () => {
  setUser((prev) => ({ ...prev, age: prev.age + 1 }));
};

関数型updateとスプレッド構文の組み合わせは、オブジェクトstate更新の事実上の標準パターンです。「prev からスプレッドで新参照を作る」――この呼吸を体に染み込ませてください。

配列stateの追加・削除・更新

const [todos, setTodos] = useState([]);

// 追加: スプレッドで新配列
const add = (text) =>
  setTodos((prev) => [...prev, { id: crypto.randomUUID(), text, done: false }]);

// 削除: filterで該当除外
const remove = (id) =>
  setTodos((prev) => prev.filter((t) => t.id !== id));

// 特定要素を更新: mapでimmutableに置換
const toggle = (id) =>
  setTodos((prev) =>
    prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
  );

push/splice/sortなどの破壊的メソッドは絶対に使わないのが鉄則です。これらは元配列を変更するため、参照が変わらず再レンダリングが起きません。concatfiltermapslicetoSorted(ES2023)などの非破壊メソッドを使います。

ネスト構造はImmer・useReducerを検討する

state構造が深くネストしてくると、スプレッドだけでは記述が爆発します。

// ネストが深い例
setData((prev) => ({
  ...prev,
  user: {
    ...prev.user,
    profile: {
      ...prev.user.profile,
      address: {
        ...prev.user.profile.address,
        city: "Tokyo",
      },
    },
  },
}));

こうなったらImmer(useImmer)やuseReducerに切り替えるサインです。あるいはstate構造そのものを見直すのが本質的解決策で、深いネストをやめてフラットなIDマップ化することが多いです。

useStateとよく一緒に登場するAPIの関係性

useStateは単体で使うより、他のHookと連携することで力を発揮します。useEffect・useReducer・useMemo・useCallback・useRefとの関係性を整理します。

主要HookとuseStateの役割比較

API 主な役割 再レンダリングを起こす? useStateとの典型的な使い分け
useState 単純なローカル状態 起こす フォーム値・トグル・カウンタ等
useReducer 複雑な状態遷移 起こす アクション数が多い/state構造が複雑
useRef レンダリング非依存の値保持 起こさない DOM参照/前回値の保持/タイマーID
useMemo 計算結果の参照固定 起こさない stateから派生する重い計算
useCallback 関数の参照固定 起こさない setterや派生関数を子に渡すとき
useSyncExternalStore 外部ストアの購読 起こす Zustand/Reduxなど外部state

「stateにすべきか、refにすべきか」の判断軸

初心者がよくやらかすのが、「画面に表示されない値までuseStateで持つ」ことです。タイマーIDや前回のスクロール位置など描画に影響しない値useRefで持ち、setStateで描画を発火させない方が正しい設計です。

function StopWatch() {
  const [time, setTime] = useState(0);
  // ⭕ 描画に関係ないタイマーIDはuseRefで保持
  const timerRef = useRef<number | null>(null);

  const start = () => {
    timerRef.current = window.setInterval(() => {
      setTime((t) => t + 1);
    }, 1000);
  };

  const stop = () => {
    if (timerRef.current) clearInterval(timerRef.current);
  };
}

判断軸はシンプルで、「その値が変わったとき、画面を再描画する必要があるか」。Yesならstate、Noならrefです。

派生stateはstateにしない

もう1つの頻出アンチパターンが、「stateから計算できるものを別のstateで持つ」です。

// ❌ アンチパターン: itemsから計算できるcountをstateで二重管理
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);

// ⭕ 派生値は計算式 or useMemoで
const items = useState([])[0];
const count = items.length; // または useMemo で派生

2つのstateが「同期しなければならない関係」になったら、設計ミスです。Single source of truth(信頼できる唯一の情報源)を1つに絞り、残りは派生として導出します。

state構造設計の3原則

useStateは便利すぎるため、「とりあえずstateにすればいいや」でstateだらけのコンポーネントが生まれがちです。state設計には明確な指針があります。

原則1: 関連するstateはまとめる

常に同時に更新される値は、別々のuseStateにせずオブジェクトで持ちます。

// ❌ 別管理は同期バグを生みやすい
const [x, setX] = useState(0);
const [y, setY] = useState(0);

// ⭕ 「座標」という単位でまとめる
const [pos, setPos] = useState({ x: 0, y: 0 });

原則2: 矛盾を防ぐためにstateを分割する

逆に、「同時に成立しえない状態」はboolean複数で表現してはいけません。フラグ複数で状態を表現すると無効な状態の組み合わせが生まれます。

// ❌ isLoading=true && isError=true といった矛盾状態が表現できてしまう
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [data, setData] = useState(null);

// ⭕ ステータスは文字列リテラルユニオンで排他的に
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");

これはState Machineの考え方で、TypeScriptと組み合わせると無効状態をコンパイル時に弾けます。XStateやTypeScriptのユニオン型を駆使すると安全度が一気に上がります。

原則3: stateは可能な限りローカルに置く

「念のため親に持たせる」「とりあえずグローバルstateに入れる」をやると、再レンダリング範囲が無駄に広がります。「実際に使うコンポーネントの最も近い共通祖先」に置くのが正解。Reactチームの公式ドキュメントでも繰り返し強調されている設計原則です。

useStateの代替を検討すべき5つの局面

useStateは万能ではありません。以下の状況では他の手段を検討します。

1. アクションが多く・状態遷移が複雑 → useReducer

type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset" }
  | { type: "set"; value: number };

function reducer(state: number, action: Action) {
  switch (action.type) {
    case "increment": return state + 1;
    case "decrement": return state - 1;
    case "reset": return 0;
    case "set": return action.value;
  }
}

function Counter() {
  const [count, dispatch] = useReducer(reducer, 0);
  return <button onClick={() => dispatch({ type: "increment" })}>{count}</button>;
}

「useStateを5個以上並べたい」「state更新ロジックがコンポーネント外でテストしたい」と感じたら、useReducerに切り替えるサインです。Reduxを使わなくても、useReducer + Contextだけで小〜中規模アプリのグローバルstateは十分回せます

2. 複数コンポーネントで共有したい → Context / Zustand / Jotai

「親の親の親…」とpropsを延々バケツリレーするprops drillingが発生したら、useContext状態管理ライブラリの出番です。

  • Zustand: 最小APIで小〜中規模に最適。Provider不要・セレクタで分割購読でき、useState感覚で書ける。
  • Jotai: アトムベース。「stateの最小単位」を分けて宣言的に組み合わせるのが得意。Recoilライクで学習コスト低め。
  • Redux Toolkit: 大規模アプリ・厳格な状態遷移管理・DevToolsで履歴を追いたいケースの定番。
  • Valtio: Proxyベースで「直接代入で更新できる」スタイル。Immer不要で書ける。

useStateは「コンポーネント内に閉じる状態」、ストアは「複数コンポーネントで共有する状態」と役割が違います。混同せず、使い分けることが重要です。

3. サーバーデータ → React Query / SWR / RSC

APIから取ってくるデータをuseStateで持つのは、もはやアンチパターンに近づきつつあります。キャッシュ・再取得・楽観的更新・サスペンス対応などサーバーデータ特有の関心事は、TanStack Query(React Query)・SWRなどのサーバーステート管理ライブラリに任せるのが2026年の標準です。Next.js 14+のRSC(React Server Components)を使えば、そもそもクライアントstateで持たない設計も可能です。

4. フォーム → React Hook Form / Conform

1つ2つの入力ならuseStateで足りますが、複雑なフォームはReact Hook Formなどが圧倒的に楽です。再レンダリング回数を最小化しつつ、バリデーション・送信・エラー表示まで宣言的に書けます。

5. URL同期したい → useSearchParams / nuqs

「タブの選択状態」「フィルタ条件」など、URLに反映したい状態をuseStateで持つと、リロードで消えて戻る/進むも壊れます。Next.jsならuseSearchParams+useRouternuqsのような型安全なライブラリでURLそのものをstate化するのが正解です。

StrictMode・React Compiler・React 19の影響

useStateを取り巻く環境は、React 18・19で大きく変わりました。最新の挙動を押さえておきます。

StrictModeでの二重実行に注意

開発時のStrictModeでは、関数コンポーネントが意図的に二重実行されます。lazy initializationの関数も、setterに渡したupdater関数も、開発中は2回呼ばれます。純粋関数として書いておけば問題ありませんが、副作用を仕込むとバグが顕在化します。

// ❌ updaterに副作用があると、StrictModeで2回呼ばれて壊れる
setItems((prev) => {
  saveToLocalStorage(prev); // 副作用!
  return [...prev, newItem];
});

// ⭕ updaterは純粋に。副作用はuseEffectで
setItems((prev) => [...prev, newItem]);

React Compilerでメモ化の手間が減る

React 19で正式版を迎えたReact Compilerは、useStateから派生する値や関数の自動メモ化を可能にしました。これにより、useMemouseCallbackを手書きする頻度は大きく減りますが、useStateそのものの書き方は変わりません。useStateで持つ値、setStateの呼び出し方、関数型updateの活用――この基礎はCompiler後も変わらないコア知識です。詳細はuseMemo vs useCallback完全比較を参照してください。

React 19の新API: useオプティミスティック・useフォームステータス

React 19ではuseOptimisticuseFormStatususeActionStateといった新APIが追加され、フォームやサーバーアクションとの統合が進化しました。useStateと組み合わせて使うことで、楽観的UI更新が宣言的に書けます。

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (state, newTodo) => [...state, { ...newTodo, sending: true }]
);

これらの新APIも、根底にあるのは「stateを宣言し、setterで更新する」useStateと同じメンタルモデルです。useStateを深く理解しておけば、新Hookも自然に身につきます

パフォーマンス特性とアンチパターン

useState自体は非常に軽量ですが、使い方を誤るとアプリ全体のパフォーマンスを下げます。代表的なアンチパターンと対処を整理します。

典型的なアンチパターン早見表

アンチパターン 何が起きるか 正しい書き方
初期値に重い計算 毎レンダリングで計算が走る useState(() => init())
直前値依存に通常setter 古い値で上書きされる 関数型update
直接プロパティ変更 再レンダリングされない スプレッドで新参照
stateから計算できるstate 同期バグ温床 派生はuseMemoや式で
refで足りる値をstateに 無駄な再レンダリング useRefで保持
useEffectでpropsをstateコピー 同期遅延・無限ループ propsを直接使う/keyリセット

「propsをuseStateにコピーする」問題

外部から渡された値をuseStateの初期値に入れ、useEffectで同期する――これは典型的なアンチパターンです。

// ❌ アンチパターン
function Profile({ user }) {
  const [name, setName] = useState(user.name);
  useEffect(() => setName(user.name), [user.name]); // 同期しないと古いまま
  // ...
}

// ⭕ そもそも親のpropsをそのまま使う、または key でリセット
function Profile({ user }) {
  // 編集中の値だけuseStateで持つ
  const [draft, setDraft] = useState(user.name);
  // 親側で <Profile key={user.id} user={user} /> とすればuser切替時に自動リセット
}

「propsを内部stateに入れたい」と感じたら、本当にコンポーネント内で編集する必要があるかを問い直しましょう。多くは親で持つべき状態です。

巨大なstateより、分割された複数stateを

1つのオブジェクトに全部詰めると、どこかが変わるたびに毎回全プロパティが新参照になります。分割購読ができないため、関連のないコンポーネントまで再レンダリングされやすいのです。useReducer・状態管理ライブラリのセレクタ・コンポジション設計で、影響範囲を絞るのが望ましい設計です。

useStateを使ったカスタムフック設計

useStateの真価は、カスタムフックに包んで再利用することで発揮されます。実用的なパターンをいくつか紹介します。

useToggle: 真偽値切り替え

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle, setValue] as const;
}

// 利用
const [open, toggleOpen] = useToggle();

useLocalStorage: localStorageと同期するstate

function useLocalStorage<T>(key: string, initial: T) {
  // lazy initialization
  const [value, setValue] = useState<T>(() => {
    try {
      const raw = localStorage.getItem(key);
      return raw ? (JSON.parse(raw) as T) : initial;
    } catch {
      return initial;
    }
  });

  useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
  }, [key, value]);

  return [value, setValue] as const;
}

usePrevious: 前回の値を参照する

function usePrevious<T>(value: T) {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

useStateとuseRef・useEffectを組み合わせて、ドメインに特化した小さなHookを作る」――これがReactの典型的なコンポジションです。React Hooksの全体像はReact Hooks 完全実践ガイドで別途整理しています。

キャリア視点:useStateを深く理解すると評価されるポイント

useStateは入門書の最初に出てくるHookですが、「正しく使える人」は意外に少ないのが現場の実情です。コードレビューで以下のような指摘ができるエンジニアは、確実に評価されます。

  • 「ここは関数型updateにしないと、連続クリックで取りこぼします」
  • 「初期値にJSON.parseを入れているので、lazy initializationにしましょう」
  • isLoadingisErrorを別管理すると無効状態が表現可能になるので、statusの文字列ユニオンに変えませんか?」
  • 「これは派生stateになっているので、useMemoか純粋な式に置き換えるべきです」
  • 「タイマーIDはuseRefで持ちましょう。useStateに入れると毎回再レンダリングが走ります」

こうした観点は未経験〜中級者でも、体系的に学べば必ず身につきます。React + TypeScriptの実務スキルは、フロントエンドエンジニアの求人で最重要の必須要件と言えるレベルです。学習・キャリア戦略としては以下のような選択肢があります。

  • テックアカデミー: Reactコースは現役エンジニアのメンタリング付きで、useState・useEffect・カスタムフックを実プロジェクトで学べる構成。短期間で実務レベルに到達したい人向け。
  • 侍エンジニア: マンツーマンレッスンでオリジナルポートフォリオを作成。React/Next.jsで「自分の作りたいプロダクト」を作る学習スタイルが取れる。
  • DMM WEBCAMP: 未経験から転職保証付きでフロントエンドエンジニア・Webエンジニアを目指す王道コース。基礎から体系的にカバー。
  • レバテック: React + TypeScript経験者向けの高単価案件が豊富。フリーランス案件・正社員案件どちらも強い。
  • Geekly: Web系自社開発企業に強い転職エージェント。モダンフロントエンド経験者向けの非公開求人を多く扱う。

useStateを「使える」から「設計できる」レベルに引き上げたコードは、職務経歴書のサンプル提出やコーディングテストで明確に差がつきます。

関連記事:Reactを体系的に深掘りするための地図

本記事はuseState単体に絞った深堀りでした。Reactの全体像、TypeScript連携、JavaScript基礎を含めて学びたい場合は、以下の記事と合わせて読むと体系が一気に固まります。

FAQ:useStateで現場でよく聞かれる7問

Q1. useStateとuseReducer、どちらを選ぶべき?

A. state更新ロジックが2〜3パターンを超えたらuseReducerが目安です。フォームの送信ステータス・ウィザード・ゲーム状態など「状態遷移が明確に定義できる」ケースはuseReducerの方が圧倒的に読みやすく、テストも容易です。逆にトグルや単純なカウンタはuseStateで十分。

Q2. setStateを呼んだ直後にstateを読みたいのですが?

A. 同じレンダリング中には絶対に新しい値は読めません。これは仕様です。「次の値を知ってから別の処理をしたい」なら、変数化して両方に使うか、useEffectで反応するか、関数型updateの内側で行います。「最新値を持つref」を別途用意するパターン(useRefに同期)もよくあります。

Q3. オブジェクトのstateを毎回スプレッドで書くのが面倒です。

A. Immer(useImmer)を導入すると、直接代入のような構文で書きつつ内部的にimmutableに更新できます。ただし「state構造そのものが深すぎる」サインでもあるので、フラット化やuseReducerへの分割を先に検討するのが本筋です。

Q4. useStateの値はサーバーサイドレンダリングでどう扱われる?

A. SSR時には初期値だけが使われ、setStateはハイドレーション後に有効化されます。Next.jsで「サーバーとクライアントで初期値が違う」とハイドレーションエラーが出るのは、useStateの初期値にブラウザ依存の値(windowlocalStorageDate.now()等)を入れた典型例。useEffect内で更新するか、SSRを無効化("use client"+クライアント限定マウント)で回避します。

Q5. 同じ値でsetStateを呼んだら本当に何も起きない?

A. プリミティブ・同じ参照のオブジェクトは何も起きません(Object.is比較で等価とみなされ、再レンダリングがスキップされます)。ただしオブジェクト・配列で新しい参照を渡すと、中身が同じでも再レンダリングは走ります。これはuseStateの仕様であり、不要な再レンダリングを避けたければ「変更がないなら同じ参照を返す」updaterを書きます。

Q6. useStateとContextを組み合わせれば、Redux/Zustandは要らない?

A. 小〜中規模なら十分です。「グローバルに共有する状態が10個未満」「分割購読が不要」ならContext+useStateで足ります。逆に「Contextの値が変わるたびに全購読コンポーネントが再レンダリングされる」性質がボトルネックになる規模では、Zustand・JotaiなどでセレクタベースのSubscribeに切り替えます。

Q7. useStateの値が「reactiveに」変わったことを別のstateに反映したいのですが?

A. ほとんどのケースはuseEffectではなく、レンダリング中に派生計算するだけで足ります。「stateAが変わったらstateBも更新」のような連鎖が多発するなら、Bを派生値(計算式やuseMemo)に変えるのが本筋。useEffectでstateを連鎖更新するのはReact公式ドキュメントでも繰り返し「避けるべきパターン」として強調されています。

まとめ:useStateは「Reactの呼吸」

useStateはReactでもっとも基本的なHookですが、「状態を持つ」という行為のすべてが詰まっていると言っても過言ではありません。シグネチャ・初期値の評価タイミング・lazy initialization・関数型update・immutable更新・派生stateの排除・refとの使い分け・useReducer/外部ストアへの卒業ライン――この一連の知識が、Reactアプリの品質を決定づけます。最後にもう一度、本記事の核を凝縮します。

  1. 初期値の評価コストが0でないなら、必ずlazy initializationを使う
  2. 直前のstateに依存する更新は、必ず関数型updateで書く
  3. オブジェクト・配列は必ず新しい参照で更新する(破壊的変更は厳禁)
  4. 派生stateは作らず、レンダリング中の計算かuseMemoで導出する
  5. 描画に影響しない値はuseRef、複雑な遷移はuseReducer、共有stateはZustand/Jotaiなど適材適所で卒業する

これを徹底するだけで、useStateを起点としたバグの大半は消えます。さらにReact 19・React Compilerの時代になっても、useStateを正しく書く技術はそのまま土台として残り続けます。Hooks全体の関係性・TypeScript連携・パフォーマンス最適化まで含めた地図は本記事の関連記事リンクから辿ってください。useStateを「Reactの呼吸」として体に染み込ませ、現場で評価されるReactエンジニアへの第一歩を踏み出していきましょう。

コメント

タイトルとURLをコピーしました