React公式サイトのドキュメントが2023年3月16日に改訂されました(「Introducing react.dev」参照)。本稿は、基本解説の「Queueing a Series of State Updates」をかいつまんでまとめた記事です。ただし、コードにはTypeScriptを加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。
なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。
状態変数を設定すると、レンダリングはキューに入ります。けれど、つぎのレンダリングがキューに加えられる前に、変数値に複数の操作を行いたい場合もあるでしょう。そのためには、Reactが状態の更新をどうバッチ処理するのかを理解しておくと役立ちます。
Reactによる状態更新のバッチ処理
たとえば、つぎのコードで[+3]ボタンをクリックしたら、カウンターの状態変数(number
)の値はいくつ加算されるでしょう。以下のサンプル001のCodeSandboxのコードで結果は確かめられます。
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}
>
+3
</button>
</>
);
}
サンプル001■React + TypeScript: Queueing a Series of State Updates 01
ボタンクリックするたびに設定関数setNumber(number + 1)
を3回呼び出しているにもかかわらず、変数値は1ずつしか加算されません。レンダリングごとに状態変数値は固定されるからです(「レンダリングはそのときとったスナップショット」参照)。つまり、はじめてボタンをクリックしてレンダリングされるとき、イベントハンドラ内で何度設定関数(setNumber()
)を呼び出して加算しても、対象となる状態変数(number
)の値は0
のまま変わりません。
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
ここで、もうひとつ押さえておくべきことがあります。Reactはイベントハンドラの中のコードがすべて実行されるのを待ったうえで、状態を更新するのです。そのため、setNumber()
の呼び出しがすべて終わってから、再レンダリングされることになります。
レストランでウェイターが注文を取るのと似ているかもしれません。ウェイターは料理をひとつ注文するたびに、厨房に駆け込んだりしないでしょう。テーブルの注文をひととおり聞くはずです。その間に、料理を変更することや、同席の人の注文も受けつけます。
こうして、無駄な再レンダリングは省いて、複数の状態変数が更新できます。コンポーネントが複数あっても同じです。これは、イベントハンドラとその中のコードを処理し終えるまで、UIが更新されないことも意味します。この「バッチ処理」と呼ばれる機能により、Reactアプリケーションの実行は大幅に速められるのです。また、一部の変数を更新していない「中途半端」なまま、レンダリングされることも避けられます。
他方で、Reactはクリックのような意図的なイベントを、複数バッチ処理することはありません。クリックごとに個別に扱います。Reactがバッチ処理するのは、一般に安全とみなせるときだけです。たとえば、ボタンクリックでフォームを無効にすれば、つぎのクリックで送信されてしまうことはありません。
レンダリング前に同じ状態変数を複数回更新する
使うことは少ないものの、つぎにレンダリングする前に、同じ状態変数を複数回更新することもできないわけではありません。つぎの状態変数値は、設定関数にsetNumber(number + 1)
のように引数値で書き替えるのでなく、つぎの状態を計算するコールバック関数として渡すことです(「関数型の更新」参照)。setNumber((n) => n + 1)
のようにすると、キュー内の直前の状態をもとにつぎの値が計算されます。Reactに、書き替える値をそのまま渡すのでなく、状態値をどう扱えばよいのか定めるのです。結果の違いは、以下のサンプル002でお確かめください。
<button
onClick={() => {
setNumber((n) => n + 1);
setNumber((n) => n + 1);
setNumber((n) => n + 1);
}}
>
+3
</button>
サンプル002■React + TypeScript: Queueing a Series of State Updates 02
設定関数(setNumber()
)の引数に渡したコールバックは「更新関数」と呼ばれます。
- Reactは更新関数をキューに入れ、イベントハンドラのコードがすべて実行されたあと呼び出して処理します。
- つぎのレンダリング時に、Reactはキューを処理したうえで、最終的に更新された状態が返ります。
つぎのレンダリング時に呼び出したuseState
について、Reactはキューを順に進みます。前回の状態変数number
の値は0でした。Reactがはじめの更新関数に引数n
として渡すのはその値です。すると、Reactはその更新された戻り値を、つぎの更新関数に与える引数n
とします。これが順に繰り返されるのです。
キューに入った更新 | n | 戻り値 |
---|---|---|
(n) => n + 1 |
0 |
0 + 1 = 1 |
(n) => n + 1 |
1 |
1 + 1 = 2 |
(n) => n + 1 |
2 |
2 + 1 = 3 |
前掲のコード例で、Reactはこのようにして3
を最終的な値として保持し、useState
から返します。そのため、[+3]ボタンのクリックで、状態変数値は3加算されたのです。
状態変数値を書き替えたあと更新した場合
状態変数値を書き替えたあと、更新関数で値を処理した場合はどうなるでしょう。結果は、以下のサンプル003で確認してください。
<button
onClick={() => {
setNumber(number + 5);
setNumber((n) => n + 1);
}}
>
Increase the number
</button>
サンプル003■React + TypeScript: Queueing a Series of State Updates 03
イベントハンドラは、Reactにつぎのように告げます。
-
setNumber(number + 5)
: 前回の状態変数number
の値は0
。Reactは、0 + 5 = 5
への「書き替え」をキューに入れます。 -
setNumber((n) => n + 1)
:(n) => n + 1
は更新関数。Reactはこの「更新関数」をキューに加えます。
キューに入った更新 | n | 戻り値 |
---|---|---|
「5 への書き替え」 |
0 (使用せず) |
5 |
(n) => n + 1 |
5 |
5 + 1 = 6 |
つぎのレンダリング時、Reactはキューを順に進みます。そして、6
を最終的な値として保持し、useState
から返すのです。値の書き替えというのは、更新関数として((n) => x)
が渡され、引数が使われないと捉えることもできます。
状態変数値を更新したあと書き替えた場合
更新関数で状態変数値が処理されたあと、さらに値を書き替えた場合はどうでしょうか。設定関数setNumber()
のはじめのふたつの呼び出しは前掲コード例と同じです。そのあと、setNumber(42)
を加えました。
<button
onClick={() => {
setNumber(number + 5);
setNumber((n) => n + 1);
setNumber(42);
}}
>
Increase the number
</button>
サンプル004■React + TypeScript: Queueing a Series of State Updates 04
3つめの設定関数の呼び出しは、状態変数の値を42
に書き替えます。Reactは42
を最終的な値として保持し、useState
から返すのです。なお、3つめの設定関数をsetNumber(number + 42)
としても結果は変わりません。状態変数number
の値は前回の0
に固定されているからです。ふたつめの設定関数に渡した更新関数の戻り値6
は使われません。
キューに入った更新 | n | 戻り値 |
---|---|---|
「5 への書き替え」 |
0 (使用せず) |
5 |
(n) => n + 1 |
5 |
5 + 1 = 6 |
「42 への書き替え」 |
6 (使用せず) |
42 |
設定関数setNumber()
に、更新関数と書き替え値をそれぞれ渡した場合についてまとめましょう。
-
更新関数(例:
(n) => n + 1
): キューには更新関数が加えられて値を処理する。 -
値の書き替え(例:
number = 5
): キューに「5
への書き替え」が加えられ、それまでのキューで処理した値は上書きされる。
イベントハンドラのコードを実行し終えると、Reactの再レンダリングが開始されます。再レンダリング時にReactにより進められるのがキューの処理です。更新関数はレンダリング中に実行されます。そのため、結果のみを返す純粋な関数でなければなりません。更新関数の中から状態を設定したり、他の副作用を実行することは許されないのです。
なお、StrictMode
が有効な開発時には、Reactは更新関数をそれぞれ2回ずつ呼び出します(ただし、2回目の結果は破棄)。これはコードの問題を見つけやすくするためです(「React + TypeScript: React 18でコンポーネントのマウント時にuseEffectが2度実行されてしまう」参照)。
命名規則
更新関数の引数名には、状態変数の頭文字を用いるのが一般的です。
setEnabled((e) => !e);
setLastName((ln) => ln.reverse());
setFriendCount((fc) => fc * 2);
コードをより詳しく書きたいときは、setEnabled((enabled) => !enabled)
のように状態変数名をそのまま使ったり、setEnabled((prevEnabled) => !prevEnabled)
のような接頭辞が添えられることもあります。
まとめ
この記事では、つぎのような項目についてご説明しました。
- 状態を設定しても、既存のレンダリングの変数は変わりません。新たなレンダリングが求められます。
- Reactはイベントハンドラの実行が終わったあと、状態の更新を処理します。これがバッチ処理です。
- 1回のイベントで状態を複数回更新するには、
setNumber((n) => n + 1)
といった更新関数を用います。