React18のAutomaticBatchingを試してみた

最近 React18beta がリリースされたので紹介していきます。

## React18 の新機能

React18 で追加される大きな新機能は下記の通りです。

- Concurrent Rendering（並行レンダリング）
- SSR support for Suspense（サスペンスの SSR サポート）
- Automatic Batching（自動バッチ処理）
- Selective hydration（選択的ハイドレーション）
- Built-in Cache（組み込みキャッシュ機構）

今回は自動バッチ処理を React17 と比較してみたいと思います。

## インストール

```
create-react-app ${プロジェクト名}

npm install react@beta react-dom@beta
```

## AutomaticBatching（自動バッチ処理）を検証

今までは関数内でのみバッチ処理を行っていました。  
しかし、React18 からはどこから発生したか関係なくすべてのレンダリングがバッチ処理されるようになります。

creat-react-app のコードを流用して実装します。

React18 を導入するためには ReactDOM.render から ReactDOM.createRoot への移行が必須です。

```js
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

↓

const container = document.getElementById("root");

const root = ReactDOM.createRoot(container);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

検証用のコードです。
onClick で clickHandler 関数を実行します。
clickHandler 関数は setTimeout で非同期でステートを更新します。

```js
const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const [count3, setCount3] = useState(0);

  const clickHandler = () => {
    setTimeout(() => {
      setCount1((count) => count + 1);
      setCount2((count) => count + 1);
      setCount3((count) => count + 1);
    }, 500);
  };

  console.log("rendering!!!");

  return (
    <div className="app">
      <header className="app-header">
        <img src={logo} className="app-logo" alt="logo" />
        <p>{count1}</p>
        <p>{count2}</p>
        <p>{count3}</p>
        <div className="push" onClick={clickHandler}>
          push
        </div>
      </header>
    </div>
  );
};

export default App;
```

以前の React であれば非同期処理内のステート更新は別々のレンダリングでした。

しかし、React18 ではバッチ処理が行われ、一回でレンダリングしているのがわかります。

- React17
![react-17.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/742770/9b325bbc-c9f5-97ae-7c92-e124ba00ec99.gif)

- React18
![react-beta.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/742770/02d3c856-b1e4-03c3-368e-5f74799b8c46.gif)

## バッチ処理をオプトアウトできる API

flushSync でバッチ処理をオプトアウトできます。

```js
import { flushSync } from "react-dom";

const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const [count3, setCount3] = useState(0);

  const clickHandler = () => {
    setTimeout(() => {
      // setCount1だけバッチ処理されない
      ReactDOM.flushSync(() => {
        setCount1((count) => count + 1);
      });
      setCount2((count) => count + 1);
      setCount3((count) => count + 1);
    }, 500);
  };

  console.log("rendering!!!");

  return (
    <div className="app">
      <header className="app-header">
        <img src={logo} className="app-logo" alt="logo" />
        <p>{count1}</p>
        <p>{count2}</p>
        <p>{count3}</p>
        <div className="push" onClick={clickHandler}>
          push
        </div>
      </header>
    </div>
  );
};

export default App;
```

- React18
![react-beta2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/742770/c318a6af-ed2e-0298-c8c8-fc0f46bd375a.gif)

  flushSync のコールバック関数内に切り出せばバッチ処理されません。

## さいごに

```
React 18 における並行レンダリングはオプトインになるため、コンポーネントの振る舞いにおいてデフォルトで大きな破壊的変更があるということはなくなります。
いつものメジャーリリースの時に要する労力と大差ないレベルの最小限の書き換えで、あるいは書き換えゼロで、React 18 にアップグレードすることができます。
「React 18に向けてのプラン」より引用
```

[公式](https://ja.reactjs.org/blog/2021/06/08/the-plan-for-react-18.html)に書かれている通り StrictMode で実装していればほぼゼロコストでマイグレーションが可能です。破壊的変更（過去のコードに影響する変更）はオプトインになり、 Automatic Batching などのあまり影響のない変更はデフォルトで有効になるそうです。  
React18 のアップデートするだけでもパフォーマンス向上が見込めるので、まだ一般向けリリースまで数か月ありますがアップデートに向けて準備しておくのもいいかもしれません。