第11回

useTransitionとuseDeferredValue:入力と一覧の更新を分ける

検索入力と一覧の更新を分けるサンプルでuseTransitionとuseDeferredValueを解説します。更新の優先度、memoとの関係、別スレッドや通信の間引きとの違いを確認します。

·7分で読める

制作・運営:React Guide 運営者

後回しにするのはReactの更新

useTransition は、入力などを優先しながら、急がないReactの更新を進めるために使います。重いJavaScriptを別スレッドで実行する仕組みではありません。

// このfilterItemsはその場で実行される。重い計算自体は後回しにならない。
startTransition(() => setResults(filterItems(query)))

startTransitionに渡した関数はすぐ実行されます。そこで長い同期計算をすれば入力も止まります。大きな計算そのものが問題なら、アルゴリズム・データ量・Web Workerなどを別に考えます。

入力値と結果表示用の値を分ける

以下は src/App.tsx の例です。重いリストをmemoされた別コンポーネントに置き、急ぐ入力の更新では古い検索条件でリストを再利用できるようにします。

import { memo, useState, useTransition } from 'react'

const items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `教材 ${i}` }))
const Results = memo(function Results({ query }: { query: string }) {
  const found = items.filter(item => item.name.includes(query))
  return <ul>{found.map(item => <li key={item.id}>{item.name}</li>)}</ul>
})

export default function App() {
  const [input, setInput] = useState('')
  const [query, setQuery] = useState('')
  const [isPending, startTransition] = useTransition()

  return (
    <main>
      <label>教材検索<input value={input} onChange={e => {
        const value = e.currentTarget.value
        setInput(value)
        startTransition(() => setQuery(value))
      }} /></label>
      {isPending && <p role="status">結果を更新中...</p>}
      <div aria-busy={isPending}><Results query={query} /></div>
    </main>
  )
}

制御されたinputの値はTransitionで更新しません。setInputは通常の更新、結果用のsetQueryだけをTransitionにします。最終結果は最後の入力と一致する必要があります。

端末が速ければpending表示は見えないこともあります。この例は速度差の測定結果ではありません。また、Reactは長い1つの同期関数の途中を自由に中断できるわけではありません。

useDeferredValueで受け取る側を調整する

更新元を変えずに表示側で遅延した値を使う場合は、useDeferredValueが選択肢です。propsだけでなく、自分のstateにも使えます。

// 上のResultsを使う別案。useDeferredValueとuseStateのimportが必要。
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery

return (
  <>
    <input aria-label="検索" value={query} onChange={e => setQuery(e.target.value)} />
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      <Results query={deferredQuery} />
    </div>
  </>
)

Resultsのmemoがないと、急ぐ入力のレンダーでもリストを再計算し、期待した効果が得られない場合があります。

debounceや通信の制御とは異なる

useDeferredValueに「300ms待つ」という固定時間はありません。1文字ごとのHTTPリクエストを自動的に減らすものでもありません。一定時間入力が止まるのを待つなら useDebounceを使います。

React 19では非同期ActionをTransitionで扱えますが、await後のsetterを再びstartTransitionで包む必要がある場面や、リクエストの到着順を管理する必要があります。Transitionを加えるだけで取得競合が解消するとは考えないでください。

試した後に確認する

入力を連続で変え、最後の検索語に対応するリストが表示されることを確認します。入力を消したら全件へ戻るかも見ます。応答性は同じ端末・件数・操作で比較し、処理量そのものが大きいならリストの仮想化や件数制限も検討します。

React公式:useTransition、useDeferredValue

#react#hooks#useTransition#react18#performance#intermediate