一連の状態更新のキューイング

状態変数を設定すると、別のレンダリングがキューイングされます。しかし、次のレンダリングをキューイングする前に、値に対して複数の操作を実行したい場合があります。これを行うには、React が状態更新をどのようにバッチ処理するかを理解することが役立ちます。

学習内容

  • 「バッチ処理」とは何か、そしてReactがそれをどのように使用して複数の状態更新を処理するか
  • 同じ状態変数に連続して複数の更新を適用する方法

React は状態更新をバッチ処理します

「+3」ボタンをクリックすると、setNumber(number + 1)を3回呼び出すため、カウンタが3回インクリメントされると予想されるかもしれません。

import { useState } from 'react';

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>
    </>
  )
}

しかし、前のセクションで思い出したかもしれませんが、各レンダリングの状態値は固定されています。そのため、最初のレンダリングのイベントハンドラ内のnumberの値は、setNumber(1)を何回呼び出しても常に0になります。

setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

しかし、ここでもう1つの要因が作用しています。Reactは、状態更新を処理する前に、イベントハンドラ内のすべてのコードの実行を待ちます。これが、再レンダリングがこれらのsetNumber()呼び出しのにのみ発生する理由です。

これは、レストランでウェイターが注文を受ける様子を思い出させるかもしれません。ウェイターは、最初の料理の名前を聞いただけで厨房に走ったりしません!代わりに、注文が終わるまで待って、変更を受け付け、テーブルの他の人の注文も受けます。

An elegant cursor at a restaurant places and order multiple times with React, playing the part of the waiter. After she calls setState() multiple times, the waiter writes down the last one she requested as her final order.

イラストレーション レイチェル・リー・ネイバーズ

これにより、複数の状態変数(複数のコンポーネントからの変数も含む)を更新しても、多くの再レンダリングがトリガーされるのを防ぐことができます。しかし、これはまた、イベントハンドラとその中のコードが完了するまで、UIが更新されないことを意味します。この動作はバッチ処理とも呼ばれ、React アプリケーションの実行速度を大幅に向上させます。また、一部の変数だけが更新された、混乱を招く「半分完成した」レンダリングに対処する必要もなくなります。

Reactは、クリックなどの複数の意図的なイベントにわたってバッチ処理を行いません。各クリックは個別に処理されます。React は、一般的に安全な場合にのみバッチ処理を行うのでご安心ください。これにより、たとえば、最初のボタンクリックでフォームが無効になった場合、2回目のクリックでフォームが再送信されることはありません。

次のレンダリングの前に同じ状態を複数回更新する

これは珍しいユースケースですが、次のレンダリングの前に同じ状態変数を複数回更新したい場合は、setNumber(number + 1)のように次の状態値を渡す代わりに、キュー内の前の状態に基づいて次の状態を計算する関数(例:setNumber(n => n + 1))を渡すことができます。これは、状態値を単に置き換えるのではなく、「状態値で何かをする」ようにReactに指示する方法です。

今、カウンタをインクリメントしてみてください。

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 1);
        setNumber(n => n + 1);
      }}>+3</button>
    </>
  )
}

ここで、n => n + 1更新関数と呼ばれます。これを状態セッターに渡すと

  1. Reactはこの関数をキューに追加し、イベントハンドラ内の他のすべてのコードが実行された後に処理されます。
  2. 次のレンダリング時に、React はキューを処理し、最終的に更新された状態を取得します。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

イベントハンドラの処理中にReactがこれらのコード行を処理する方法を次に示します。

  1. setNumber(n => n + 1)n => n + 1は関数です。Reactはこれをキューに追加します。
  2. setNumber(n => n + 1)n => n + 1は関数です。Reactはこれをキューに追加します。
  3. setNumber(n => n + 1)n => n + 1は関数です。Reactはこれをキューに追加します。

次のレンダリング中にuseStateを呼び出すと、Reactはキューを処理します。前のnumberの状態は0であったため、Reactは最初の更新関数にn引数として渡します。次にReactは、前の更新関数の戻り値を取得し、次の更新関数にnとして渡します。これを繰り返します。

キューイングされた更新n返します
n => n + 100 + 1 = 1
n => n + 111 + 1 = 2
n => n + 122 + 1 = 3

Reactは3を最終結果として格納し、useStateから返します。

これが、上記の例で「+3」をクリックすると値が3だけ正しくインクリメントされる理由です。

状態を置き換えた後に状態を更新するとどうなるか

このイベントハンドラーはどうなりますか?次のレンダリングでnumber はどうなると思いますか?

<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
      }}>Increase the number</button>
    </>
  )
}

このイベントハンドラーはReactに以下の処理を実行するように指示します。

  1. setNumber(number + 5)number0なので、setNumber(0 + 5)となります。Reactはキューに「5に置き換える」を追加します。
  2. setNumber(n => n + 1)n => n + 1は更新関数です。Reactはその関数をキューに追加します。

次のレンダリング時に、Reactは状態キューを処理します。

キューイングされた更新n返します
5に置き換える」0(未使用)5
n => n + 155 + 1 = 6

Reactは最終結果として6を格納し、useStateから返します。

注記

setState(5)は実際にはsetState(n => 5)のように動作しますが、nは未使用です!

状態を更新した後に状態を置き換えるとどうなるか

もう1つの例を試してみましょう。numberが次のレンダリングでどうなると思いますか?

<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
        setNumber(42);
      }}>Increase the number</button>
    </>
  )
}

このイベントハンドラーの実行中に、Reactがこれらのコード行をどのように処理するかを示します。

  1. setNumber(number + 5)number0なので、setNumber(0 + 5)となります。Reactはキューに「5に置き換える」を追加します。
  2. setNumber(n => n + 1)n => n + 1は更新関数です。Reactはその関数をキューに追加します。
  3. setNumber(42):Reactはキューに「42に置き換える」を追加します。

次のレンダリング時に、Reactは状態キューを処理します。

キューイングされた更新n返します
5に置き換える」0(未使用)5
n => n + 155 + 1 = 6
42に置き換える」6(未使用)42

次に、Reactは最終結果として42を格納し、useStateから返します。

setNumber状態セッターに渡すものの考え方についてまとめます。

  • 更新関数(例:n => n + 1)がキューに追加されます。
  • その他の値(例:数値5)は、「5に置き換える」をキューに追加し、既にキューに入っているものは無視します。

イベントハンドラーが完了した後、Reactは再レンダリングをトリガーします。再レンダリング時に、Reactはキューを処理します。更新関数はレンダリング中に実行されるため、更新関数はピュアである必要があり、結果を返すだけです。更新関数内から状態を設定したり、その他の副作用を実行したりしないでください。Strictモードでは、Reactは各更新関数を2回実行し(2回目の結果は破棄されます)、間違いを見つけるのに役立ちます。

命名規則

更新関数の引数を、対応する状態変数の最初の文字で名前付けるのが一般的です。

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

より冗長なコードを好む場合は、setEnabled(enabled => !enabled)のように状態変数の完全な名前を繰り返すか、setEnabled(prevEnabled => !prevEnabled)のようにプレフィックスを使用する別の一般的な規則があります。

要約

  • 状態の設定は、既存のレンダリングでの変数を変更しませんが、新しいレンダリングを要求します。
  • Reactは、イベントハンドラーの実行が完了した後に状態の更新を処理します。これはバッチ処理と呼ばれます。
  • 1つのイベントで状態を複数回更新するには、setNumber(n => n + 1)更新関数を使用できます。

チャレンジ 1 2:
リクエストカウンターの修正

ユーザーがアートアイテムに対して同時に複数の注文を送信できるアートマーケットプレイスアプリに取り組んでいます。「購入」ボタンを押すたびに、「保留中」カウンターが1ずつ増加する必要があります。3秒後、「保留中」カウンターが減少し、「完了」カウンターが増加する必要があります。

しかし、「保留中」カウンターは意図したとおりに動作しません。「購入」をクリックすると-1に減少します(これは不可能です!)。そして、素早く2回クリックすると、両方のカウンターが予測不可能に動作するようです。

なぜこれが起こるのでしょうか?両方のカウンターを修正してください。

import { useState } from 'react';

export default function RequestTracker() {
  const [pending, setPending] = useState(0);
  const [completed, setCompleted] = useState(0);

  async function handleClick() {
    setPending(pending + 1);
    await delay(3000);
    setPending(pending - 1);
    setCompleted(completed + 1);
  }

  return (
    <>
      <h3>
        Pending: {pending}
      </h3>
      <h3>
        Completed: {completed}
      </h3>
      <button onClick={handleClick}>
        Buy     
      </button>
    </>
  );
}

function delay(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}