useStateを理解したい
Reactの状態管理で基本的なフックのuseState。
useStateはどんなもので、どのように使うのかを解説していきます。
そもそもReactで状態管理が必要な理由とは?
useStateを理解する前に、Reactではなぜ状態管理が必要なのかを簡単に説明します。
Reactは仮想DOMという仕組みを使って画面を効率的に更新します。
仮想DOMは「設計図の比較」によって、変更箇所だけを実際のDOMに反映します。
この仕組みが動作するには、Reactが「変更があった」ことを知る必要があります。
それを可能にするのが状態管理です。
状態変更
↓
Reactが検知
↓
新しい仮想DOMを作成
↓
旧仮想DOMと比較
↓
差分を計算
↓
最小限のDOM操作
↓
画面更新
つまり、状態管理は、仮想DOMの仕組みを動かすトリガーです。
状態管理は状態を適切に扱うことであり、
- 状態を保存する
- 状態を変更する
- 状態の変更を画面に反映させる
useStateを使うことで、Reactがこれらを自動的に処理し、仮想DOMによる効率的な画面更新が実現されます。
状態管理が必要なケース vs 不要なケース
状態管理が不要なケース
状態管理が不要なのは、一度表示したら変化しないページです。
例えば、会社概要ページ、利用規約ページ、静的なブログ記事などは、ユーザーが見ている間に内容が変わることはありません。これらのページでは、HTMLのように固定された情報を表示するだけなので、状態管理は必要ありません。
// 状態管理が不要な例:変更されない静的なページ
function AboutPage() {
return (
<div>
<h1>会社概要</h1>
<p>私たちの会社は2020年に設立されました。</p>
<p>所在地: 東京都渋谷区</p>
<p>従業員数: 50名</p>
</div>
);
}
このようなコンポーネントは、一度レンダリングされたら、ページを閉じるまで同じ内容が表示され続けます。ユーザーが何かをクリックしても、時間が経過しても、画面は変わりません。つまり、「変化するデータ」を管理する必要がないため、useStateなどの状態管理は不要です。
状態管理が必要なケース
一方、状態管理が必要なのは、ユーザーの操作や時間の経過で画面が変化する場合です。
例:フォーム入力
ユーザーがテキストボックスに文字を入力すると、入力内容を追跡する必要があります。入力された値を保持し、送信時にその値を使ったり、リアルタイムでバリデーションを行ったりするために、状態管理が必須です。
function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// ユーザーが入力するたびに状態が更新され、画面に反映される
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="お名前"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="メールアドレス"
/>
<p>入力内容: {name} ({email})</p>
</form>
);
}
状態管理するかの判断基準
シンプルに言うと
- 変化しない = 状態管理不要
- 変化する = 状態管理必要
状態とはアプリケーションの「現在の状況」を表すデータです。
ユーザーの操作、サーバーからのデータ取得、時間の経過など、何らかの理由で画面の内容が変わる可能性がある場合は、その「変化するもの」を状態として管理する必要があります。
useStateを学ぶ
それではここからはuseStateを学んでいきましょう。
useStateの基本
const [state, setState] = useState(initialState);
// ↑ ↑ ↑
// 状態変数 更新変数 初期値
useStateは3つで構成されています。
- 状態変数(state)・・・現在の状態を保持する。読み取り専用として扱う。
- 更新関数(setState)・・・状態を更新するための関数。この関数を呼ぶとReactが再レンダリングをトリガーする
- 初期値(initialState)・・・最初にレンダリングされるときの値。数値や文字列、配列、オブジェクトなどを指定可能
宣言する箇所
コンポーネントのトップレベルで useState を呼び出して、state 変数を宣言します。
トップレベルとは関数コンポーネントの上部のことです。
条件分岐の中やループの中、try-catch文の中などで宣言してはいけません。
// ❌ 間違った例
function BadExample({ condition }) {
const [name, setName] = useState('');
if (condition) {
const [age, setAge] = useState(0); // 条件分岐の中でuseState
}
const [email, setEmail] = useState('');
}
// ✅ 正しい例
function UserExample({ condition }) {
// 全てのフックをトップレベルで宣言
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
// 条件分岐は使用時に行う
return (
<div>
<p>Name: {name}</p>
{condition && <p>Age: {age}</p>} {/* ここで条件分岐 */}
<p>Email: {email}</p>
</div>
);
}
命名規則
Reactの慣習として、以下の命名規則があります。
- 状態変数はわかりやすい名詞にする
- 更新関数は
set
+ 状態変数名(キャメルケース)
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// ❌ 悪い例
const [count, updateCount] = useState(0); // "set"を使わない
const [count, change] = useState(0); // 意味が不明確
const [x, setX] = useState(0); // 変数名が不明確
const [Count, SetCount] = useState(0); // 大文字始まりは避ける
状態変数にsetを付けたものが更新関数なので理解しやすくなります。
初期値の型
useStateは様々なデータ型を初期値として指定できます。
// 数値
const [count, setCount] = useState(0);
const [age, setAge] = useState(25);
// 文字列
const [name, setName] = useState('');
const [message, setMessage] = useState('Hello');
// 真偽値
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(false);
// 配列
const [items, setItems] = useState([]);
const [todos, setTodos] = useState(['買い物', '掃除']);
const [numbers, setNumbers] = useState([1, 2, 3]);
// オブジェクト
const [user, setUser] = useState({ name: '', age: 0 });
const [form, setForm] = useState({
email: '',
password: '',
rememberMe: false
});
// null/undefined
const [data, setData] = useState(null);
const [error, setError] = useState(undefined);
どんな型の値でも渡すことができますが、関数を渡した場合は特別な振る舞いをします。この引数は初回レンダー後は無視されます。
状態の更新
状態を更新するには、必ず更新関数(setState)を使います。
更新関数を使わない場合、画面の変化(再レンダリング)が起こりません。
状態は直接変更してはいけない
状態を直接変更することはしてはいけません。(そもそもconst
で宣言されていると再代入はできませんが。。。)
// ❌ 間違い:直接変更しようとする
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
count = count + 1; // エラー!constは再代入できない
};
return <button onClick={increment}>+1</button>;
}
// ✅ 正しい:更新関数を使う
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // 更新関数を使う
};
return <button onClick={increment}>+1</button>;
}
状態更新の3パターン
パターン1:直接値を渡す
現在の状態に関係なく、特定の値をセットしたいときなど。
const [count, setCount] = useState(0);
const reset = () => {
setCount(0);
};
const setToTen = () => {
setCount(10);
};
return (
<div>
<p>カウント: {count}</p>
<button onClick={reset}>リセット</button>
<button onClick={setToTen}>10にする</button>
</div>
);
パターン2:現在の状態を使って更新
現在の状態をもとに新しい値を計算するときなど。
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // 現在の状態を参照
};
const double = () => {
setCount(count * 2); // 現在の状態を計算に使う
};
return (
<div>
<p>カウント: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={double}>2倍</button>
</div>
);
}
関数内で連続して更新すると、前の状態を参照して思ったのと違う結果になることがあります。
const incrementThree = () => {
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1(古いcountを参照)
setCount(count + 1); // 0 + 1 = 1(古いcountを参照)
// 結果: count = 1(期待: 3)
};
パターン3:関数を渡す(前の状態を使う)
最新の状態を確実に参照したいときや、連続して更新するとき、イベントループ・タイマー内での更新のとき
関数を使うと、古い状態を参照してしまうことがなくなります。
思った動きにならない場合はパターン3を使うのがよさそうです。
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prevCount => prevCount + 1); // 関数を渡す
// ^^^^^^^^^
// 前の状態が自動で渡される
};
const incrementThree = () => {
setCount(prev => prev + 1); // 0 + 1 = 1
setCount(prev => prev + 1); // 1 + 1 = 2
setCount(prev => prev + 1); // 2 + 1 = 3
// 結果: count = 3 ✅
};
return (
<div>
<p>カウント: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={incrementThree}>+3</button>
</div>
);
}
配列・オブジェクトの更新
Reactを学習していると、必ず見ることになるのが、スプレッド構文です。
LPやWordPressのサイトなどを制作していて、jQueryでコードを書いていても使うことはないのかなと思います。
Reactではスプレッド構文などで配列を新しく作ることをよく行います。
仮想DOMの仕組みというのもありますが、Reactはパフォーマンスのために状態が変更されたかどうかを「参照の比較(===)」で判断します。
完全一致していたら更新しない、一致してなければ更新対象になります。
ひとつひとつのページの状態は膨大な量になります。
仮想DOMオブジェクトが複雑になればなるほど比較に時間がかかってしまいます。
すべてを細かく見て間違い探すするよりも、新しくなっているとわかれば内容を見なくても更新対象とすることができますよね。
間違い探しは労力がかかる
サイゼリヤの間違い探しは面白いけど、大変ですよね。
集合写真 | 集合写真 |
---|---|
![]() |
![]() |
同じ名前で部分的に違う場合はなかなか注意深くみないと分かりません。
集合写真 | チアリーディング |
---|---|
![]() |
![]() |
それが、違う名前、中身も違ったら誰でもすぐ分かりますよね?
// 間違い探し(重い処理)
const oldArray = [1, 2, 3];
const newArray = [1, 2, 4]; // 3が4に変更
// 全ての要素を比較する必要がある
function findDifferences(old, new) {
// 0番目: 1 === 1 ✓
// 1番目: 2 === 2 ✓
// 2番目: 3 !== 4 ← 違いを発見!
}
// 参照チェック(高速)
const sameArray = oldArray;
const differentArray = [...oldArray];
console.log(oldArray === sameArray); // true → 変更なし
console.log(oldArray === differentArray); // false → 変更あり(一瞬で判定)
Reactの高速検知
Reactは参照(メモリアドレス)が変わったかどうかだけを見て、高速に変更を検知します。
新しい配列・オブジェクトを作成せずに、直接変更すると参照自体は変わらずその変更を検知することができません。
// ❌ 間違った更新(検知されない)
const [items, setItems] = useState([1, 2, 3]);
const addItem = () => {
items.push(4); // 直接変更
setItems(items); // 同じ参照 → 検知されない
};
// ✅ 正しい更新(検知される)
const addItem = () => {
setItems([...items, 4]); // 新しい配列 → 検知される
};
更新関数の役割
const [state, setState] = useState(initialValue);
↑これ
更新関数は「更新」とはありますが、役割としては「新しい状態を保存して予約する」ことです。更新関数を呼び出すことですぐに再レンダリングが行われるわけではありません。保存し、予約されるだけです。
function ConfusingExample() {
const [items, setItems] = useState([1, 2, 3]);
const addItem = () => {
setItems([...items, 4]); // 「更新関数」だから更新された?
console.log(items.length); // まだ 3 !
};
}
更新関数を実行しても値はすぐには変わっていません。
次のレンダリングが行われたときに状態が変化します。
function BetterUnderstanding() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 「次回レンダリング時、count は 1 にしてください」
setCount(1);
// 現在のレンダリングでは、まだ count は 0
console.log('現在のcount:', count); // 0
// 次回レンダリングで count は 1 になる
};
}
なぜこのような仕組みなのか?
Reactは効率的に再レンダリングを行うためにバッチ処理によって最適化しています。
お客さん: 「コーラください」
店員: 「厨房にコーラ1つ!」→ 厨房が動く
お客さん: 「ハンバーガーも」
店員: 「厨房にハンバーガー1つ!」→ 厨房がまた動く
お客さん: 「アイスも」
店員: 「厨房にアイス1つ!」→ 厨房がまた動く
結果:厨房が3回動く(非効率)
お客さん: 「コーラください」「ハンバーガーも」 「アイスも」
店員: 「少々お待ちください...」(注文をまとめる)
店員: 「厨房に、コーラ1つ、ハンバーガー1つ、アイス1つ!」
厨房: 「了解、まとめて作ります」
結果:厨房が1回だけ動く(効率的)
Reactが最適なタイミングで実際の更新を実行し、複数の更新は自動でバッチ処理されます。そして仮想DOMで差分を計算してまとめ、再レンダリングしてDOMが変更されます。
Reactの処理の流れ:
1. 複数のsetState → バッチ処理でまとめる
2. 新しい仮想DOM → 前回との差分を計算
3. 必要な部分のみ → 実際のDOMを更新
まとめ
useStateの重要なポイント
- トップレベルで宣言
-
命名規則を守る (
[value, setValue]
) - 3つの更新パターン
- 配列・オブジェクトは新しい参照を作成する
- 更新は予約制
Reactの基本的な状態管理フックのuseStateについて使い方などについてまとめてみました。最後までお読みいただきありがとうございます。