第11回
useTransitionとuseDeferredValue:入力と一覧の更新を分ける
検索入力と一覧の更新を分けるサンプルでuseTransitionとuseDeferredValueを解説します。更新の優先度、memoとの関係、別スレッドや通信の間引きとの違いを確認します。
制作・運営: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を加えるだけで取得競合が解消するとは考えないでください。
試した後に確認する
入力を連続で変え、最後の検索語に対応するリストが表示されることを確認します。入力を消したら全件へ戻るかも見ます。応答性は同じ端末・件数・操作で比較し、処理量そのものが大きいならリストの仮想化や件数制限も検討します。
同じテーマの教材
useCallbackで関数をメモ化:React.memoと組み合わせた最適化
ReactのuseCallbackを解説します。関数のメモ化・React.memoとの組み合わせ・useEffectの依存配列での活用を実例で解説。useMemoとの違いと使い分けも明確にします。
useMemoでパフォーマンス最適化:重い計算をメモ化する
ReactのuseMemoを解説します。メモ化の仕組み・高コスト計算の最適化・参照の安定化、そしてuseMemoを使うべき場面・使わなくてよい場面の判断基準を実例付きで学べます。
React Compiler 1.0:Viteへの導入とメモ化の確認方法
安定版になったReact Compilerを実際に試す。Vite 8環境への導入からDevToolsでの効果確認、既存のuseMemoとの付き合い方までを動かしながら解説する。