useStateの使い方・状態が更新されない理由と対処法【React】
この記事のポイント
ReactのuseStateは状態変数と非同期の更新関数をペアで定義し、配列やオブジェクトの更新時にはスプレッド構文等で新たな参照を作成して不変性を保ち、正確な値の反映には関数形式の更新を利用することでバグやパフォーマンス低下を防ぐ堅牢な状態管理を実現します。
「useStateの正しい使い方や、更新した値が即座に反映されない理由を知りたい」と考えていませんか。Reactのレンダリングの仕組みまで深く理解して、バグのないコードを書きたいという悩みは多いものです。
こうした疑問にお答えします。
本記事の内容
- useStateの基本構文とデータ型ごとの実装手順
- 更新が即時反映されない仕様や配列・オブジェクト操作の注意点
- パフォーマンス低下を防ぐためのアンチパターンと回避策
useStateはReactでの開発において、動的なUIを実装するために欠かせないフックです。状態管理の正しい手順とイミュータビリティの概念を理解することは、非常に重要なポイントといえます。
この記事を読めば、useStateのset関数による更新タイミングの落とし穴を回避し、堅牢でメンテナンス性の高いモダンなコードが書けるようになるはずです。さっそく詳細を確認していきましょう。
useStateの基本概念
Reactを用いたフロントエンド開発において、useStateは最も基本的で重要なフックです。従来の関数コンポーネントは表示するだけでしたが、useStateの登場で動的なデータを保持できるようになりました。
プログラミングの変数は値を一時的に格納しますが、useStateの状態はUI表示と密接に結びつきます。useStateの使い方は初期値を受けて、現在の状態と更新関数をペアで返すのが基本です。
Reactにおける状態の役割
Reactとは何かを踏まえると、状態(state)はコンポーネントの状況を記憶するメモリのような役割を果たします。ボタンのクリック回数やフォーム入力など、ユーザー操作で変化する情報はすべてuseStateで管理し、その他の用途はReact Hooksの全体像 で網羅される各種フックで対応します。
状態を定義する理由は、データ変化時にUIを自動で最新の状態へ同期させるためです。useState更新タイミングに合わせて画面表示が自動で最適化されるため、手動のDOM操作は必要ありません。
値を保持する仕組み
useStateが値を保持する仕組みは、React内部のデータ構造に関連しています。通常の変数は関数終了時に破棄されますが、状態はReactのメモリ領域に保存され次回のレンダリングまで保持されます。
Reactはフックが呼び出された順番で状態を識別します。そのため、useState関数は必ずトップレベルで呼び出す必要があり、if文やループ内での使用は禁止です。
| 項目 | プリミティブ型(数値・文字列) | オブジェクト・配列 |
|---|---|---|
| 更新方法 | 新しい値をそのままセットする | スプレッド構文などで新しいコピーを作る |
| 具体例 | setCount(1) | setUser({...user, name: 'Alice'}) |
| 注意点 | 値の比較が容易 | 参照が変わらないと再表示されない |
再レンダリングが発生する条件
Reactコンポーネントの再レンダリング は、主にuseStateのset関数が実行されたときにトリガーされます。状態が更新されると、Reactはそのコンポーネントと子コンポーネントを再計算して差分を反映します。
再レンダリングを効率化するために、Reactには特有のルールが存在します。
- 値の同一性チェック:新しい値が現在と同じなら再レンダリングをスキップする
- バッチ処理:複数の更新をまとめて1回の再表示で処理し効率を高める
- 非同期な性質:set関数呼び出し直後は古い値のままである
新しい値に基づいた処理が必要な場合は、useState関数に前の値を受け取るコールバックを渡します。useState Reactの仕組みを理解することで、バグのないモダンなコードが書けるようになります。
useStateの実装手順
Reactで動的なUIを構築するためには、状態管理が不可欠です。useStateはReactで状態(state)を保持し、変化に応じて画面を再描画する最も基本的なフックといえます。
React 19を含む最新バージョンでも、useStateの使い方は変わりません。性質を正しく理解することで、バグの少ない効率的な開発が可能になります。
① 関数をインポートする
useStateを使用するための最初のステップは、Reactライブラリから関数をインポートすることです。
useStateは名前付きエクスポートとして提供されているため、中括弧を使用してインポートしてください。useEffectなどのフックを併用する場合も、明示的なインポートが必要です。
- インポートの記述例:
import { useState } from 'react' - 注意点:フックは必ずコンポーネントのトップレベルで呼び出す必要があります。
② 初期値を定義する
次に、コンポーネントの内部でuseStateを宣言し、初期値を設定します。
useState関数は、現在の状態値と更新用の関数の2つを配列形式で返します。これを分割代入で受け取るのが一般的な書き方です。
| 指定する方法 | 書き方の例 | 特徴 |
|---|---|---|
| 直接値を指定 | useState(0) | 最も一般的な指定方法。 |
| 初期化関数を指定 | useState(() => compute()) | 初回のみ実行されるためパフォーマンスが向上する。 |
初期値の設定によって、扱うデータの型を定義します。フォーム値を管理する場合は空文字列、リストを管理する場合は空の配列を初期値にするのが定石です。
③ 更新関数を実行する
状態を変更する際は、useStateのset関数(慣習的にsetから始まる名前)を使用します。
状態更新は非同期である点に注意が必要です。更新関数を呼び出した直後に元の変数を確認しても、新しい値は反映されていません。
- 直接更新:
setCount(10)- 新しい値を直接引数として渡します。
- 関数による更新:
setCount(prev => prev + 1)- 現在の値に基づき計算する場合に推奨される方法です。
配列やオブジェクトを更新する場合、元の状態を直接書き換えてはいけません。スプレッド構文などを用いて新しいオブジェクトを作成し、不変性を保持することが必須です。
④ TypeScriptの型を指定する
TypeScriptを使用する場合、useState<T> のように型引数を渡すと型安全性が高まります。
単純な値は初期値から推論されますが、複雑なオブジェクトや初期値がnullのケースでは明示的な型指定が不可欠です。
- 基本の型指定:
useState<number>(0) - 複雑なオブジェクト:
useState<User | null>(null) - 配列の指定:
useState<string[]>([])
型を指定すれば、誤った型のデータを渡した際にエラーとして検知できます。なお、NuxtなどReact以外のフレームワークでは書き方が異なるため、Reactの仕様を正しく把握しましょう。
useStateを更新する際の注意点
useStateはReact開発で最も頻繁に使うフックですが、正確な挙動の理解が欠かせません。正しく扱わないと、意図しないバグやパフォーマンス低下を招く恐れがあります。
状態更新のタイミングやデータの扱いには、React特有のルールが存在します。useStateを安全に更新するために、知っておくべき4つの重要ポイントを確認しましょう。
更新が即時反映されない仕様
useStateのset関数を呼び出しても、直後に変数の値が切り替わるわけではありません。Reactの状態更新は非同期で行われ、次回の再レンダリング時に新しい値が反映されます。
更新の流れは次のとおりです。
- 更新関数を実行して状態の変更を予約する
- Reactが再レンダリングをスケジュールする
- コンポーネントが再実行され、画面に新しい値が表示される
set関数を呼び出した直後の行で変数を参照しても値は古いままである点に注意が必要です。新しい値が反映されたタイミングで処理を行いたい場合は、useEffectを使って次回のレンダリング後に確認してください。また、複数の更新は1回にまとめるバッチング処理によって効率化されます。
過去の値を参照する不具合の対処法
現在の状態を元に計算する場合、変数を直接参照するとバッチ処理の影響を受けます。例えば setCount(count + 1) を2回続けても、同じ古い値を参照するため1しか増加しません。
この問題を解決するには、useStateの更新関数に別の関数を渡す書き方を使います。
- 修正が必要な例:
setCount(count + 1) - 推奨される書き方:
setCount((prev) => prev + 1)
この関数型アップデートでは、引数として常に最新の状態が保証されます。連続した更新や非同期処理の中でも、正しい値を基準に計算が可能です。
配列の直接変更によるエラーの回避法
Reactにおいて、状態は不変である必要があります。配列の更新時に push や splice を使うと、既存の配列自体を書き換えてしまいます。
Reactは配列の参照先が変わらないと変更を認識できず、再レンダリングが実行されません。スプレッド構文などを用いて、新しい配列を作成してセットしましょう。
- 要素の追加:
setItems([...items, newItem]) - 要素の削除:
setItems(items.filter(item => item.id !== targetId)) - 要素の更新:
setItems(items.map(item => item.id === targetId ? newValue : item))
常に新しい参照を作成することで、Reactへ正しく変更を通知できます。
オブジェクトの安全な書き換え方
オブジェクト形式の状態管理でも、不変性の維持が非常に重要です。一部のプロパティだけを書き換える場合でも、オブジェクト全体を新しく作り直します。
既存のオブジェクトを一部だけ更新する手順は以下の通りです。
- スプレッド構文で既存のプロパティをすべてコピーする
- 変更したい特定のプロパティだけを上書きする
- 作成した新しいオブジェクトをset関数へ渡す
具体的なコードの書き方を確認しましょう。
setUserInfo({
...userInfo,
age: 30
});
コピーし忘れたプロパティは消失するため、注意深く記述する必要があります。新しいオブジェクトとして再構築する意識が、安全な状態管理の基本です。
useStateで避けるべきアンチパターン
React開発においてuseStateは欠かせないフックですが、設計を誤るとバグや速度低下を招きます。React 18以降の最適化を活かすため、正しい状態管理の知識が必要です。
ここでは、開発現場で陥りやすい代表的なアンチパターンを3つの観点から解説します。
導出できる値の二重管理
既存のpropsや他のstateから計算できる値を、個別のuseStateで管理するのは避けましょう。
例えば、姓と名のstateがある場合にフルネームを別のuseStateで保持し、useEffectで同期させる実装がこれに該当します。この方法ではレンダリングが2回発生し、データの不整合が起きるリスクも高まるでしょう。
フルネームのような導出可能な値は、レンダリング内で変数として直接計算するだけで十分です。これにより再レンダリングは1回に抑えられ、同期ロジックも不要になるためコードがシンプルになります。計算コストが高い処理に限り、useMemoを使用して計算結果をメモ化してください。
重い処理を初期値に渡す実装
useStateの初期値に重い計算や関数の戻り値を直接渡すと、アプリのパフォーマンスが劣化します。
初期値に渡されたコードは、2回目以降の再レンダリングで無視される場合でも、毎回実行されてしまいます。ローカルストレージからの読み込みなどをそのまま渡すと、画面更新のたびに処理が走り、UIが固まる原因となります。
この問題を解決するには、以下の「遅延初期化」を活用してください。
- 初期値を直接渡さず、初期値を返す関数を渡す
useState(() => getHeavyData())のように記述する
これにより、Reactは初回レンダリング時のみ関数を実行し、以降はスキップします。React 18の機能を活かすためにも、初期値を一度だけ計算させる実装を心がけてください。レンダリング後に発生する重い計算については、useMemoで重い処理を最適化 する手段も併用するのが定石です。
複雑な状態を無理に扱う設計
useStateを過剰に分割したり、関連する値をバラバラに管理したりする設計は可読性を損ないます。
状態が複雑化すると更新タイミングの把握が困難になり、バグの温床となります。以下のような兆候がある場合は、設計を見直すべきです。
- x座標とy座標のように密接な値を別々のstateで宣言している
- 一つの操作で5つ以上のset関数を連続で呼び出している
- stateを同期させるためだけにuseEffectを多用している
複雑な状態を整理するには、以下の改善が効果的です。
- 関連する値のグループ化:密接な値はオブジェクトとして一つのuseStateにまとめる
- useReducerの導入:状態遷移のロジックが複雑な場合にロジックを分離する
- カスタムフックの作成:状態管理とUIを切り離し、コードをクリーンに保つ
- React Contextでの状態共有:複数階層に渡って参照される値はContextで配信する
- React Reduxによる状態管理:大規模で複雑なフローはストアで一元管理する
React 18の最新仕様に合わせ、公式が推奨する宣言的な状態管理を徹底しましょう。
まとめ:useStateを正しく理解して状態管理しよう
本記事では、React開発の基本であるuseStateの役割や基本的な書き方、再レンダリングの仕組みを解説しました。状態管理の要となるインポート手順や初期値の設定方法、配列やオブジェクトを扱う際のイミュータビリティの大切さなど、実践的な知識を網羅しています。
useStateの使い方は、配列の分割代入を利用して状態変数とset関数を定義する流れが基本です。Reactにおける状態更新のタイミングは非同期で行われるため、古い値を参照しないよう関数形式の更新も活用しましょう。
NuxtなどReact以外のフレームワークでも状態管理は重要ですが、Reactではこのフックが中心的な役割を果たします。useEffectとの組み合わせや型の指定方法など、正しい知識を身につけることが大切です。
本記事のポイント
- useStateは分割代入を使い、状態変数と更新関数をセットで定義する
- 状態更新は非同期なため、正確な値の反映には関数形式の更新が有効
- オブジェクトや配列の更新では、元の値を変更せず新しい参照を作成する
useStateの基礎から応用までをマスターすれば、バグの少ないモダンなアプリケーションの実装が可能です。状態の変化とレンダリングの連動を理解して、ユーザー体験の高いUI構築に役立ててください。
開発における高度な状態管理や、プロジェクトのパフォーマンス最適化にお悩みではありませんか。困ったときは、ぜひお気軽にご相談ください。
useStateに関するよくある質問
参考文献
執筆者
編集部
Next.jsやAIを活用したモダンWeb開発・SEO実装に関する情報を発信。SEOに最適化したモダンWebサイト制作、設計ノウハウ、構造化データや内部リンク設計などを中心に扱っています。
監修者
MT Templates 代表/編集長
海外メディア企業でSEOエディターとして従事後、独立。複数メディア運営の経験をもとに、Next.jsやAIを活用したWeb開発・SEO技術を発信。リード獲得につながるサイト構築からSEO設計まで一貫したサポートを提供している。
関連記事
Reactのライフサイクルの仕組みとuseEffectでの実装【図解】
旧機能の廃止や再描画に悩む方へ、Reactのライフサイクルを図解し、useEffect等のフックによるアンマウント制御を学ぶことで、最適な実装が可能です。
Reactのコンポーネントの作り方・分け方・設計【初心者向け】
Reactのコンポーネントの適切な分け方や作り方に悩む方へ、種類や使い方、設計、ライブラリまで解説し、実務で活きる高保守性コード習得を導きます。
ReactのUIライブラリ人気7選・要件別の徹底比較【プロ解説】
UI開発に悩む方へ、人気のReactのUIライブラリを解説し、Material UI等の活用で技術的負債を防ぎ、美しいUIデザインによる保守性の高い開発を実現します。
useMemoの使い方・使わない基準とは?useCallbackとの違い
ReactでuseMemoの用途にお悩みですか。useCallbackやuseEffectとの違い、使わない基準を解説。不要な再レンダリングを防ぎ、アプリを最適化できます。
ReactとRedux入門・Toolkitの全5つの実装手順【初心者向け】
ReactでReduxを導入したい方向けに、ToolkitやTypeScriptでの実装手順から使わない条件まで解説し、実務的な状態管理スキルが身につく入門記事です。
ReactのContextの使い方とアンチパターン【プロが徹底解説】
ReactのContextでPropsバケツリレーを解消する使い方を解説。再レンダリングのアンチパターンやReduxとの比較を通じ、保守性の高い実装が可能です。