Reactで動的なUIを構築する鍵は、コンポーネントの「記憶」であるstateを適切に管理することにあります。
本記事では、stateとは何か?stateを更新するセッター関数(setIndexなど)とは何か?予期せぬバグを防ぐため、直前の値 (prev) に基づいた確実な更新を行うためのテクニックなどを、コード例で解説しています。
stateとは何か?
state(ステート)とは、Reactのコンポーネントが内部で持っている、変化する可能性のあるデータのことです。
これらの保持しているデータが変更されると、Reactは自動的にコンポーネントを再描画(リレンダリング)し、画面/UIを最新の状態に更新します。
このため、stateは、ユーザーの操作や時間の経過などによって画面の表示内容が変わる場合に必須です。
- ボタンが押された回数(カウンターの値)
- 入力フォームに入力されたテキスト
- 画像カルーセルの現在表示されている画像のインデックス
- 商品のカートに入っているアイテムのリスト
- メニューが開いているか閉じているか(真偽値)
stateがないとどうなるか?
Reactにおいて「stateがない」とは、静的なデータや普通のJavaScript変数のみを使っている状態です。
この場合、一度画面が表示(レンダー)された後は、ページをリロードしない限り、ユーザー操作などに合わせた再読込み(レンダー)を行いません。
このため、ユーザーが操作をしてもコンポーネントの内容が変わりません。
onClick属性でイベントハンドラを指定することで、alertやconsoleなどを実行することができます。これはブラウザの外部機能を使う処理だからです。コンポーネントなど画面上の表示を変更するにはstateがな必須です。
stateの使い方(useStateフック、セッター関数)
stateを使う際は、useStateというフックを使ってstateを管理します。
また、stateの値を更新するにはセッター関数を使います。
useStateの呼び出し
useStateを使うには、ReactのライブラリからuseStateを読み込む必要があります。
import { useState } from 'react';useStateから2つの要素を受け取り、初期値を設定する
useStateは2つの要素を持つ配列を返します。
- 1つ目の要素: 現在のStateの値(読み取り専用。変数名の例:indexやcount)
- 2つ目の要素: Stateを更新するための関数(セッター関数。変数名の例:setIndexやsetCount)
これらの値を分割代入で変数名を指定して受け取ります。
useStateには引数として初期値を渡します。
例えば、カウンターでcountという変数に入った数値を更新していく場合は以下のように記述します。(初期値は0)
const [count, setCount] = useState(0);セッター関数の処理を記述する
イベントの発生に伴い、設定した変数を更新するために、セッター関数setCountの処理を記述します。
setCount(count + 1)処理を記述する
setCountをイベントハンドラとして、もしくは、イベントハンドラの中の処理として記述します。
例えば、ボタンタグの中のonClick属性に設定する場合は以下のようにします。
<button onClick={() => setCount(count + 1)}>カウンター</button>イベントハンドラで直接setCountを渡すと引数が渡せないので新しい値の計算をすることができません。
<button onClick={setCount}>カウンター</button>もしくは、イベントハンドラの中の処理としてsetCountを記述します。
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => {
setCount(count + 1);
};
return (
<button onClick={incrementCount}>カウンター</button>
);
}実例
上記の処理をまとめると以下のようになります。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => {
// setCountを呼び出して、新しい値を設定する
setCount(count + 1);
};
return (
<button onClick={incrementCount}>カウンター</button>
);
}stateの注意点
stateの値を直接変更してはいけない
Stateの値を直接変更してはいけません。直接変更すると、ReactはStateが変更されたことを検知できず、UIの再描画が行われません。
Stateを更新するときは、必ずsetCountのようなセッター関数を使用します。
| NG | OK |
count = count + 1; | setCount(count + 1); |
user.age = 31; | setUser({ ...user, age: 31 }); |
stateは即座に変更されない
セッタ―関数による更新は、すぐに実行されるのではなく、予約(キューイング)されます。そのため、更新直後に同じstate変数を参照しても、数値が更新されていない場合があります。
連続した更新を行う場合や前のstateの値に依存する場合は、関数形式のセッターで引数を使います(後述)。
stateはローカル
astateは、それを宣言したコンポーネントの中でのみ使えます(ローカル)。親から子へデータを渡したい場合はProps(属性)を使用します。
- State: コンポーネント自身が管理し、時間とともに変化するデータ。
- Props: 親コンポーネントから子コンポーネントへ渡されるデータ(通常は不変)。
更新直後の値を使う(prevState, prevCountなど)
ReactのState更新関数(例: setCount(newValue))は、すぐにStateを変更するのではなく、その変更を予約(キューに入れる)します。
このため、stateの新しい値が、直前のStateの値に依存している場合、意図した処理にならないことがあります。
ダメな例
次のように、stateの現在の値countを使って新しい値を計算するとします。
function incrementTwice() {
setCount(count + 1); // 1回目の更新予約
setCount(count + 1); // 2回目の更新予約
}この場合、ユーザーがボタンを押し、incrementTwiceが実行された時点のcountが5だとします。
- 最初の
setCount(count + 1)は、現在のcountである5を使用して、5 + 1 = 6を次のStateとして予約します。 - 2番目の
setCount(count + 1)も、まだStateが更新されていないため、現在のcountである5を使用して、5 + 1 = 6を次のStateとして予約します。
結果、Stateは6になり、期待していた7になりません。
対処法:セッター関数に関数を渡す
セッター関数の第1引数には更新が処理される直前の値が必ず入ります。このため、この引数を用いた処理を記述します。
なお、直前の値を表す引数には「prev~」という変数名を使うことが一般的です(prevCount, prevIsOn, prevItemsなど)。
| 例 | セッター |
| カウンター | setCount(prevCount => prevCount + 1); |
| トグル(真偽値の反転) | setIsOn(prevIsOn => !prevIsOn); |
| 配列・オブジェクトの更新 | setItems(prevItems => [...prevItems, newItem]); |
先ほどの、カウンターを正しく機能するように記述すると以下のようになります。
function incrementTwiceCorrect() {
setCount(prevCount => prevCount + 1); // 1回目の更新予約
setCount(prevCount => prevCount + 1); // 2回目の更新予約
}Reactは、更新が実行される際、この予約された関数にその時点での最新のStateの値(prevCount)を渡します。
最初の更新が処理されるとき、prevCountは5なので、Stateは6になります。
2番目の更新が処理されるとき、Reactは最初の更新後の値、つまり6をprevCountとして渡し、Stateは6 + 1 = 7になります。
このように、前のStateの値が正しく連結(チェーン)され、期待通りの結果を得ることができます。

