バッチ更新に対応する

状態の更新を1回のレンダリング呼び出しで組み合わせる方法

React.jsのバッチ更新

コンポーネントの状態を更新するたびに、Reactの差分アルゴリズムが仮想DOMの変更を検出し、コンポーネントを実際のDOMでレンダリングすることは既にご存知でしょう。しかし、これは実際にすべての状態更新で発生しますか?または、Reactはこれらの更新をバッチ処理する方法を提供し、複数の状態の変更が1つのレンダリング呼び出しに結合されるようにしますか?

合成イベントにより、バッチ更新が可能になります

React 17以下は、これらの更新をバッチ処理するメカニズムを提供します。コーディング中にそれについてあまり考える必要はありませんが、Reactがコードを最適化する方法を知ることは、それでも素晴らしい学習です。

これが何が起こっているのかです。

  • 複数の状態の変更がコールバック内で呼び出されます
  • コールバックが合成イベントにアタッチされている場合、Reactはそれらのミューテーションをバッチ処理します
  • ミューテーションがバッチ処理されると、レンダリング呼び出しは1回だけ行われます。
  • それ以外の場合、各突然変異は新しいレンダリングにつながります

バッチ処理を可能にするには、状態の変更を合成Reactイベント内で呼び出す必要があります。簡単に言うと、合成Reactイベントは基本的に、ネイティブHTMLイベントの単なるラッパーであり、すべてのブラウザーで同じ機能を提供します。すべての合成イベントハンドラーは、propとしてコンポーネントに直接提供できます。

すべての動作を確認するために、Reactがすべての状態のミューテーションをミューテーションごとに1つのレンダーではなく、1つのレンダーに結合できるようにするコード例を次に示します。

function Component() {
  const [id, setId] = useState(0);
  const [date, setDate] = useState(new Date());
  const [random, setRandom] = useState(Math.random());

  // A lame callback, but it's just for
  // the demo. Note that clicking on the
  // button will only lead to a single
  // re-render!
  //
  // This is b/c the callback is provided to
  // 'onClick', a synthetic event.
  const onButtonClick = () => {
    setDate(new Date());
    setId(prevId => prevId + 1);
  }
  
  console.log("Render:", id, date);

  return (
    <div>
      <button 
        onClick={onButtonClick}>
        Update state
      </button>
    </div>
  );
}
//
// Almost the same as the previous
// example, but this time w/o
// a batched upate call.
//

function Component() {
  const [id, setId] = useState(0);
  const [date, setDate] = useState(new Date());
  const [random, setRandom] = useState(Math.random());

  // No batching, as the state
  // mutations happend inside
  // a setTimeout - even though
  // the timeout is inside a 
  // synthetic event.
  const onButtonClick = () => {
    setTimeout(() => {
      setDate(new Date());
      setId(prevId => prevId + 1);
    }, 0);
  }
  
  console.log("Render:", id, date);

  return (
    <div>
      <button 
        onClick={onButtonClick}>
        Update state
      </button>
    </div>
  );
}

React18以降のバッチ更新

Reactの次のバージョンでは(少なくともこれを書いている間は)、バッチ処理のメカニズムが大幅に改善されます。合成イベントでのみ可能である代わりに、他のタイプのコールバックも使用できます。

  • タイムアウト
  • 約束
  • ネイティブイベントハンドラ

この変更により、自動的に実行されるレンダリングが少なくなるため、アプリのパフォーマンスが向上する可能性があります。

React18以降でのバッチ更新の回避

React 18以降、状態の変化ごとに新しいレンダリングを明示的にトリガーするオプションがあり、バッチ更新を回避できます。これはエッジケースでのみ使用される可能性が高いですが、それでも知っておくと興味深いでしょう。

//  
// This example is almost
// 1:1 from Dan Abramov's
// example.
//
// Use 'flushSync' from the
// 'react-dom'-package.
import { flushSync } from 'react-dom'; 

function handleClick() {
  flushSync(() => {
    setId(prevId => prevId + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setDate(new Date());
  });
  // React has updated the DOM by now
}