Reactでブロッキングが発生するときはuseTransitionで防げる

Reactは重い処理でブロッキングが発生する

Reactで開発していると、処理が重いコンポーネントやイベントハンドラが原因で、UIが一時的にフリーズしてしまう「ブロッキング」が発生します。

この問題は、特に大量のデータをレンダリングする場合や、計算負荷の高い処理を伴うコンポーネントで顕著です。

例えば、以下のように3種類のコンポーネントを3つのタブ(ボタン)で切り替えるようなケースで、2つ目のコンポーネントの処理が重い場合は、UIをブロッキングするため、処理が完了するまで他のタブ(ボタン)を押しても切り替えできなくなります。

App.tsx
import { useState } from 'react'
import TabButton from './TabButton.tsx'
import First from './First.tsx'
import Second from './Second.tsx'
import Third from './Third.tsx'

export default function App() {
  const [tab, setTab] = useState<string>('first')

  function selectTab(nextTab: string) {
    setTab(nextTab)
  }

  return (
    <>
      <TabButton
        isActive={tab === 'first'}
        onClick={() => selectTab('first')}
      >
        First
      </TabButton>
       
      <TabButton
        isActive={tab === 'second'}
        onClick={() => selectTab('second')}
      >
        Second (slow)
      </TabButton>
       
      <TabButton
        isActive={tab === 'third'}
        onClick={() => selectTab('third')}
      >
        Third
      </TabButton>
      <hr />
      {tab === 'first' && <First />}
      {tab === 'second' && <Second />}
      {tab === 'third' && <Third />}
    </>
  )
}
Second.tsx
import { memo } from 'react'

type Props = {
  index: number;
}

const Second = memo(function Second() {
  const items = []
  for (let i = 0; i < 1000; i++) {
    items.push(<SlowComponent key={i} index={i} />)
  }
  return (
    <ul className="items">
      {items}
    </ul>
  )
})

function SlowComponent({ index }: Props) {
  const startTime = performance.now()
  while (performance.now() - startTime < 1) {
    // for slow component
  }

  return <li>item{index + 1}</li>
}

export default Second

Reactでブロッキングが発生するサンプル

useTransitionでブロッキングを防げる

useTransitionはReactのフックで、非同期で実行したい状態の更新に低い優先度を設定するために使用されます。

これにより、UIが重い処理でブロッキングされるのを防げます。

使い方は const [isPending, startTransition] = useTransition() のように読み込んで、処理が重い部分をstartTransition内で実行するだけです。

TSX
import { useState, useTransition } from 'react'

// 中略
const [isPending, startTransition] = useTransition()

function selectTab(nextTab: string) {
  startTransition(() => {
    setTab(nextTab)
  })
}

isPendingは処理中はtrueになるので、「Loading...」などの表示に使えます。

TSX
{isPending && <h1>Loading...</h1>}
{!isPending && tab === 'first' && <First />}
{!isPending && tab === 'second' && <Second />}
{!isPending && tab === 'third' && <Third />}

ReactでブロッキングをuseTransitionで防ぐサンプル

useTransition()が使えるのはReact v18以降

useTransition()が使えるのはReact v18以降です。

バージョンが17以下のReactでは使用不可なので注意してください。

ブロッキングが発生しない場合は使わないほうが良い

useTransition()を使用するとタスクを分割して実行するため、処理完了までの時間が通常よりもかかります。

これによりブロッキングが防げているのですが、逆に言うとブロッキングが発生していない状態で使用すると処理時間が無駄に長くなってしまいます。

※ タスクが分割されても平行処理ではなく、1つずつ実行していくため処理時間は通常より長くなる

そのため、ブロッキングが発生していない場合はuseTransitionの使用は控えたほうが良いです。