useStateはReactの組み込みフックで、関数コンポーネントがクラスコンポーネントに変換することなくローカルステートを管理できるようにします。React 16.8でフックが導入され、useStateはその中で最も広く使われるようになり、クラスベースのアプローチからthis.stateとthis.setStateを置き換えました。Reactドキュメント(2025年)によると、最新のReactアプリケーションの80%以上のコンポーネントが、フォームデータ、インターフェースフラグ、カウンターの管理にuseStateを使用しています。フックは現在の値と、ステートを更新してコンポーネントの再レンダリングをトリガーするセッター関数からなるタプルを返します。
重要ポイント
useStateは、バージョン16.8で追加されたReactの基本的なフックで、関数コンポーネントにローカルステートを保存および変更する機能を提供します。フックが導入される前は、ステートはクラスコンポーネントでのみthis.stateとthis.setStateを介して使用でき、関数コンポーネントは純粋にプレゼンテーション用でした。useStateはこの制限を取り除き、関数コンポーネントを使用してアプリケーション全体を記述できるようにしました。
フックは1つの引数(初期ステート値)を受け取り、2つの要素を持つ配列を返します。最初の要素は現在のステート値で、2番目はそれを更新する関数です。Reactはセッター関数が安定しており、レンダリング間で変更されないことを保証するため、子コンポーネントに安全に渡したり、クロージャで使用したりできます。
React DevTools使用状況調査(2024年)によると、useStateはReactアプリケーションの96%で使用されており、エコシステムで最も一般的なフックとなっています。ReduxやZustandを介してグローバルステートを使用するアプリケーションでも、useStateによるローカルステートがUIステート(開いているモーダル、入力フィールドの値、アクティブなタブ)を管理する主要なメカニズムであり続けています。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
useStateの内部メカニズムは、Reactのファイバーノードシステムに基づいています。Reactの各コンポーネントは、それに関連付けられたフックのリンクリストを保存するファイバーノードで表現されます。コンポーネントがuseStateを呼び出すと、Reactはこのリストに新しいノードを作成し、その中に現在のステート値と更新キューの参照を保存します。
セッター関数が呼び出されると、Reactはすぐにステートを更新しません。代わりに、更新を保留中のステートキューに配置し、コンポーネントの再レンダリングをスケジュールし、次のレンダリング時にのみ、古いステートと適用された更新に基づいて新しいステートを計算します。これによりバッチ処理が保証されます。つまり、1つのイベントハンドラでセッターを3回呼び出すと、Reactはそれらを1回の再レンダリングにグループ化します。
Reactチーム — React 18ワーキンググループ(2024年)によると、React 18では自動バッチ処理が拡張され、ステートの更新はイベントハンドラだけでなく、setTimeout、Promiseコールバック、ネイティブイベントハンドラでもグループ化されるようになりました。これにより、複数のステート更新があるシナリオで最大30%のパフォーマンス向上がもたらされました。
新しいステートが前のステートに依存する場合は、セッターの関数型を使用します: setCount(prev => prev + 1)。Reactは更新時の実際のステート値を関数に渡すため、バッチ処理された更新でも正確性が保証されます。関数型を使用しない場合、setCount(count + 1)は連続して複数回呼び出されると古い値を使用する可能性があります。
// 遅延初期化 — 一度だけ実行
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// Result: count increased by 3
// NOT batch-safe: stale closure
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Result: count increased by 1 (stale closure)
useStateの基本構文は非常に簡潔です: const [state, setState] = useState(initialValue)。初期値は最初のレンダリング時のみ使用され、その後のレンダリングではReactはそれを無視して現在保存されている値を返します。初期計算にコストのかかる操作が必要な場合は、初期化関数を渡します: useState(() => computeExpensiveInitial())。
遅延初期化は、初期値がlocalStorage、複雑なデータ変換、URLパラメータの解析から得られる場合に特に重要です。Reactは初期化関数を1回だけ呼び出します(コンポーネントのマウント時)。これにより、その後のレンダリングでリソースを節約できます。遅延初期化がない場合、コストのかかる式は結果が無視されても毎回のレンダリングで評価されます。
| 形式 | 例 | 使用タイミング |
|---|---|---|
| 直接 | useState(0) | 単純な初期値 |
| 遅延 | useState(() => compute()) | コストのかかる初期計算 |
| 関数型セッター | setState(prev => prev + 1) | 前の値に基づく更新 |
| 直接セッター | setState(newValue) | 新しい値が古い値に依存しない |
// オブジェクトステート — 新しい参照を作成
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('user');
return saved ? JSON.parse(saved) : null;
});
useStateはプリミティブ(数値、文字列、ブール値)と参照型(配列、オブジェクト)の両方で同様に機能します。ただし、重要な違いがあります。プリミティブの場合、Reactは値によって変更を検出しますが、オブジェクトと配列の場合は参照によって検出します。新しいものを作成せずに既存のオブジェクトを変更しても、参照が同じままであるため、再レンダリングはトリガーされません。
オブジェクトや配列を扱うときは、変更したデータで新しいコピーを常に作成してください。オブジェクトの場合はスプレッド構文を使用します: setUser(prev => ({...prev, name: newName}))。配列の場合は、新しい配列を返すメソッド(filter、map、concat)または要素を追加するためのスプレッド構文を使用します。push、pop、spliceなどの変更メソッドは機能しません—配列の参照が変更されていないため、Reactはそれらを無視します。
// ✅ 正しい: フックはトップレベル
const [form, setForm] = useState({ name: '', email: '' });
const updateField = (field, value) =>
setForm(prev => ({ ...prev, [field]: value }));
// ...
const [items, setItems] = useState([]);
const addItem = item =>
setItems(prev => [item, ...prev]);
すべてのReactフックと同様に、useStateは2つのルールに従います。第一に、フックはコンポーネントのトップレベルでのみ呼び出し、条件文、ループ、またはネストされた関数の中に配置しないでください。これにより、フックが毎回のレンダリングで同じ順序で呼び出されることが保証され、これはReactの内部リンクリストにとって重要です。
第二のルール: フックはReact関数コンポーネントまたはカスタムフックからのみ呼び出します。通常の関数、コールバック、またはクラスコンポーネントからuseStateを呼び出さないでください。これらのルールに違反すると、Invalid hook callエラーが発生し、Reactが実行時にそれをキャッチします。
// ❌ 古いクロージャの問題
function GoodComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ✅ 関数型更新で修正済み
}
// countは古いです!
function BadComponent() {
if (isLogged) {
const [user, setUser] = useState(null);
}
}
最も一般的な間違いは、新しい参照を作成する代わりにオブジェクトと配列を直接変更することです。開発者はクラスコンポーネントの変更可能なスタイルに慣れており、this.state.user.name = ‘新しい名前’が機能していました(推奨されていませんでしたが)。関数コンポーネントでは、そのような変更は単に無視されます。Reactは参照の変更を認識できず、コンポーネントを再レンダリングしません。
古いクロージャの問題は、useEffectやイベントハンドラに渡されたコールバックが、コールバックが作成された時点のクロージャから古いステート値をキャプチャしたときに発生します。解決策は、セッターの関数型を使用するか、useEffectの依存関係配列に必要な値を含めることです。Reactドキュメント — フックFAQ(2025年)によると、古いクロージャは、誤った依存関係配列に次いで、フックのバグの2番目に一般的な原因です。
// ❌ Stale closure problem
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // count is stale!
}, 1000);
return () => clearInterval(timer);
}, []);
// ✅ Fixed with functional update
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
よくある質問
はい、ただし注意が必要です。深くネストされたオブジェクトの場合、更新時に各レベルでコピーが必要です: setState(prev => ({...prev, nested: {...prev.nested, key: newValue}}))。3つ以上のネストレベルを持つオブジェクトがある場合は、Immer(不変更新のためのライブラリ)を検討するか、異なるレベルごとにステートを複数のuseState呼び出しに分割してください。
useStateは、セッターを呼び出してもすぐに値が更新されないという意味で非同期です。Reactは更新をキューに入れ、次のレンダリングで適用します。setCount(newValue)の後も、現在の関数内のcount変数には古い値が含まれています。新しい値は、次のコンポーネント呼び出しでのみ利用可能になります。バッチ処理メカニズムにより、複数の呼び出しが1つのレンダリングにグループ化されます。
ReactはInvalid hook callエラーをスローします。フックはReact関数コンポーネントまたはカスタムフック(名前がuseで始まる関数)の内部でのみ呼び出すことができます。この制限はフックの実装に組み込まれており、フックはコンポーネントのファイバーツリーを使用してステートを保存するため、コンポーネントの外部ではこのツリーが利用できません。
useStateは単純な独立したステート(カウンター、入力フィールド、フラグ)に適しています。useReducerは、ステートが複数のフィールドを持つ複雑なオブジェクトである場合、更新ロジックがアクションタイプに依存する場合、または次のステートに複雑な変換が必要な場合に適しています。実用的なルール: 3つ以上の相互に関連するステートフィールドがある場合は、useReducerを使用してください。
これはパフォーマンス上の理由からです。useStateが同期的にステートを更新する場合、セッターを呼び出すたびに即座に再レンダリングがトリガーされ、1つのイベントハンドラ内で複数の再描画が発生します。Reactが更新をグループ化して1回の再レンダリングを実行するバッチ処理は、React 16で導入され、React 18で自動バッチ処理によって大幅に改善された最適化です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。