React18のAutomatic Batchingを試してみてわかったこと

こんにちは!フロントエンドエンジニアの川瀬です。

少し前に、SuspenseやTransitionなど楽しみにしていたReact18がリリースされたのですが、 自動バッチングというのも新要素としてあったのでどういったものかなと試してみました。

自動バッチング https://ja.reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching

バッチングとは React がパフォーマンスのために複数のステート更新をグループ化して、単一の再レンダーにまとめることを指します

試してみたreact18とreact17のソース

// App.tsx 18も17も同じ
import React from "react"

function App() {
  const [count, setCount] = React.useState(0)
  const [flag, setFlag] = React.useState(false)
  const render = React.useRef(0)  //レンダー回数を数えます
  
  render.current++

  const update = () => {
    setCount(c=>c+1)
    setFlag(f=>!f)
  }
  
  React.useEffect(()=> {
    const id = setTimeout(()=>{
      setCount(c=>c+1)
      setFlag(f=>!f)
    } ,1000)
    return ()=> clearTimeout(id)
  },[])
  

  return (
    <div style={{
      display: 'flex',
      height: '100vh',
      justifyContent: 'center',
      alignItems: 'center',
      background: '#777',
      color: '#fff'
    }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10}}>
        <div>Version: (18 or 17)</div>
        <div>render:{render.current}</div>
        <div>count:{count}</div>
        <div>flag:{flag? 'true': 'false'}</div>
        <button onClick={update}>update</button>
      </div>
    </div>
  )
}

export default App;
// index.tsx
import React from 'react';
import App from './App';
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
// 18はcreateRootを使います
import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
// StrictModeにすると開発環境ではrenderが2回走ってしまうのでやらない
root.render(
  // <React.StrictMode>
    <App />
  // </React.StrictMode>
);
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
//17
import { render } from 'react-dom';

// StrictModeにすると開発環境ではrenderが2回走ってしまうのでやらない
render(
  // <React.StrictMode>
    <App />
  // </React.StrictMode>
,document.getElementById('root') as HTMLElement);

動き確認

ブラウザ表示したばかりでは、どちらもレンダー1回目、countやflagの更新はなしです。

useEffectで、setTime内の処理が1秒後に実行され、countとflagが更新されます。 この時、React18ではrender回数は1回更新されのに対し、React17では2回更新されました。 中でstateを更新している分だけrenderが走ってしまいます。

続けてUpdateボタンで更新してみます。

ボタンで更新した場合はどちらも1回のrenderで済みました。

※公式より抜粋

自動バッチング以前は、React のイベントハンドラ内での更新のみバッチ処理されていました。promise や setTimeout、ネイティブのイベントハンドラやその他あらゆるイベント内で起きる更新はデフォルトではバッチ処理されていませんでした。

参考:https://github.com/reactwg/react-18/discussions/21 より

// promise
fetch(/*...*/).then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
})

// つまりasync awaitでも起こる
const update = async () => {
  await console.log('promise!')
  setCount(c=>c+1)
  setFlag(f=>!f)
}
// ネイティブイベント
elm.addEventListener('click', () => {
  setCount(c => c + 1);
  setFlag(f => !f);
});

非同期とかaddEventListenerとかよく使ってる、気をつけねば・・

つまり?

React17の場合、上記条件でstateが複数更新される場合、その分renderが走ってしまいます。 onClickなどのReactのイベントハンドラであれば問題なし☆

React18からは、予期せぬrenderが走らない => パフォーマンスがよくなっている、ということですね。

React17でどうしてもsetTimeoutやpromiseの中でstateを更新したい場合は、stateをまとめてみるしか?

const [state, setState] = React.useState({count:0, flag:false})

React.useEffect(()=>{
  setTimeout(()=> setState(v=>{ count: v.count+1, flag: !v.flag}),1000)
},[])

また、色々と試していきたいと思います! 最後まで読んでいただきありがとうございました。

herp.careers