💡

React はなぜバッチ処理なのか

2025/01/26に公開

本文はReactのバッチ処理に関する記事です。Reactではステート管理やレンダーの制御を細かく実装することでより良いUI/UXを提供できます。そこでなぜReactはバッチ処理をするのか、バッチ処理に関するロジックと実装方法について深掘りしていきます。

Reactにおけるバッチ処理とは

バッチ処理とは、複数の状態更新(state updates)を一括して処理する仕組みです。具体的には、イベントハンドラやその中のコードがすべて完了するまで、UIの更新を待機させます

バッチ処理のメリットとしては、大きく分けて2つあります。

  1. パフォーマンスの向上

    バッチ処理の最大のメリットは、パフォーマンスの向上です。複数の状態更新を一括で処理することで、不要な再レンダリングを減少させ、レスポンスを高速化します。

    例えばイベントハンドラ内に100回のstate更新があった場合、それらを1回のDOM操作で実行することができます。

  2. 一貫性のあるUI状態

    バッチ処理を使用することで、複数の状態更新が一度に適用されるため、UIの一貫性が保たれます。これにより、ユーザーエクスペリエンスが向上し、予期せぬUIのちらつきや不整合を防ぐことができます。

では、具体的なコード例を見てみましょう。以下のReactコンポーネントでは、ボタンをクリックすると数値が+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>
    </>
  )
}

期待される動作: ボタンをクリックすると、数値が3増える。

実際の動作: 数値は1しか増えない!

なぜこんなことが起きるのか?

これは、Reactのバッチ処理が原因です。まず、Reactはレンダーされた時、スナップショットを作成します。関数から返される JSX内のprops、イベントハンドラ、ローカル変数はすべて、レンダー時の state (スナップショットの値)を使用して計算されます。

つまりこの場合:

  1. ボタンクリック時にスナップショット作成 (setStateのレンダー実行時の値である0で固定される)
  2. setStateは同じ基準値(この場合は0)を基に計算。
  3. そのため、最後のsetNumber(number + 1)だけが実際に適用され、数値は1だけ増える。

どうすれば良いか?

では、正しく数値を正しく3に増やすためにはどう実装すれば良いでしょうか?ポイントはアップデート関数(updater function) を使用することです。

アップデート関数を使った解決策

以下のように、setNumberにコールバック関数を渡します。

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

なぜこれで動くのか?

アップデート関数を使用すると、イベント内の値はキューに格納されキュー内の最新の値にアクセスできるため、各setNumberは最新の状態を基に計算されます。その結果、数値は正しく3増えるのです。

以下の表で、このプロセスを詳しく見てみましょう。

キュー内の更新処理 n 返り値
n => n + 1 0 0 + 1 = 1
n => n + 1 1 1 + 1 = 2
n => n + 1 2 2 + 1 = 3

解説:

  1. 最初のsetNumber(n => n + 1)が呼び出され、nは現在の状態0です。返り値は1
  2. 次のsetNumber(n => n + 1)では、前回の更新でn1になった状態を基に計算され、返り値は2
  3. 最後のsetNumber(n => n + 1)では、n2となり、返り値は3に更新されます。

このように、アップデート関数を使用することで、各更新が前回の状態を正しく反映し、期待通りの結果が得られるのです。

さいごに

Reactでは細かい挙動の違いがあり、このような小さな違いがバグに繋がったり、はたまたUI/UXの向上といった大きい違いを生み出すことが多々あります。公式ドキュメントが充実しているので、適宜読み直していきたいと思います。

参考資料

https://ja.react.dev/learn/queueing-a-series-of-state-updates

https://ja.react.dev/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time

Discussion