今回のReact 18への予習シリーズはAutomatic batchingです。
実はReactのbatchingという言葉はつい最近まで知りませんでした、しかしその動きは知っていました。さらにReact17.Xまでのbatchingに問題点があることも知りませんでした。😅
Wikimedia Commons: Multiple Silos Single Weigh Static Automatic Batching System.jpg
batchingとは
以下の簡単なReactのコードは+
ボタンを押すと、numberとrepeatのステータスが更新されます(そして再表示されます)。
このコードでLog
コンポーネントはコンソールにRender
と表示するだけのコンポーネントです。これがあることで再表示が何度起きたかがわかります。
Reactではステートが変更されると再表示されます。しかしステートの変更、以下のコードではsetNumber、setRepeat関数が呼び出されるたびに再表示が行われると、表示速度が遅くなり良いUXを提供できなくなります。
このコードで+
ボタンを押すと、2つのステートが更新されます、しかしRender
は1回だけ表示されます。
このように複数のステート変更が行われても1回しか再表示が行われない仕掛けをbatchingと呼ばれています。この動作はReactを使った事のある方は知って(体験して)いると思います。
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
const Log = () => {
console.log("Render");
return null;
}
const App: React.FC = () => {
const [number, setNumber] = useState(0)
const [repeat, setRepeat] = useState(1)
const incement = () => {
setNumber(number + 1)
setRepeat(repeat + 1)
}
return (
<>
<p>{String(number).repeat(repeat)}</p>
<button onClick={incement}>+</button>
<Log/>
</>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
batchingの限界
しかし、React17.X以下ではbatchingの動作しないケースがあります、下のコードはその例です。
ステートの2つの変更がsetTimeout関数のコールバック内にあると、Render
は2回表示されます。ステート更新毎に再表示が行われているのです。
batchingの動作しないケースは、
- Promise
- setTimeout
- addEventListener()で設定したイベントハンドラー
ということで、fetch等を使った通信処理で結果を複数のステート変更に使いコードは良くあると思いますが、ここではステートの更新毎に再表示が行われています!
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
const Log = () => {
console.log("Render");
return null;
}
const App: React.FC = () => {
const [number, setNumber] = useState(0)
const [repeat, setRepeat] = useState(1)
const incement = () => {
setTimeout(() => {
setNumber(number + 1)
setRepeat(repeat + 1)
}, 1000)
}
return (
<>
<p>{String(number).repeat(repeat)}</p>
<button onClick={incement}>+</button>
<Log/>
</>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
なぜこうなるのか?
React 17.X以下のbatchingが動作するのは、
onClick, onChange...
等のイベントはReactのライブラリー内で処理されますのでステート変更処理がBatching方式になります- Batching方式ではステートの変更はReact内のキューに変更情報が追加されるだけです
- イベント処理の最後でキューに変更情報があれば再表示が1回だけ行われます
しかし、Promiseの処理コードやaddEventListener()で設定したイベントハンドラーではステート変更処理が、batching方式になっていないのでステート変更毎に再表示が実行されてしまします。
React18 Automatic batching
React 18で以下のように並列モード(Concurrent Mode)を有効にすると、Promiseの処理コードやsetTimeout、addEventListenerで設定したイベントハンドラーでもbatchingが動作しまます(Render
表示は1回だけになります)。
- ReactDOM.render(<App />, document.getElementById('root'))
+ ReactDOM.createRoot(
+ document.getElementById('root') as HTMLElement).render(<App />)
並列モード(Concurrent Mode)の表示処理Fiberは、①メモリー上の仮想DOM構築と、②それの結果を使いブラウザー画面(DOM)を更新する処理に、分かれています。
仮想DOM構築処理は小さい単位で中断(終了・再開)できるようになっています、そしてブラウザー画面を更新処理は画面のリフレッシュレート(16mSec)毎のみ行われます。
並列モード(Concurrent Mode)ではステート変更処理毎に仮想DOMは変更されますが、再表示は16mSecに1回しか行われません。したがってほとんどの場合はbatching動作します。
Fiberに付いては、A deep dive into React Fiber internalsに詳しく書かれています。
このブログでも薦めている Lin Clark - A Cartoon Intro to Fiber - React Conf 2017 はFirberの内部を漫画を使い分かりやすく解説しています。