useDeferredValue は UI の一部の更新を遅延させるための React フックです。高コストな再レンダーを後回しにすることで、ユーザー入力のレスポンシブ性を維持します。
const deferredValue = useDeferredValue(value)
1. なぜ useDeferredValue が必要か
1.1 高コストな再レンダーによる入力遅延問題
テキスト入力に連動して重いリストを再レンダーするケースを考えてみましょう。
function SearchPage() {
const [query, setQuery] = useState('');
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
{/* ❌ 重いリストがキー入力をブロック */}
<SlowList text={query} />
</>
);
}
問題: SlowList の再レンダーに時間がかかると、テキスト入力がもたつき、ユーザー体験が悪化します。
1.2 useDeferredValue が解決すること
useDeferredValue を使うと、値の更新を遅延させて、入力のレスポンシブ性を維持できます。
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // 遅延されたバージョン
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
{/* ✅ 遅延された値でレンダー、入力はスムーズ */}
<SlowList text={deferredQuery} />
</>
);
}
結果:
-
queryは即座に更新 → 入力がスムーズ -
deferredQueryは遅れて更新 → リストは後から「追いつく」 - タイピング中はリストの再レンダーが後回しに
1.3 useDeferredValue の API
const deferredValue = useDeferredValue(value, initialValue?)
| 引数 | 説明 |
|---|---|
value |
遅延させたい値。任意の型を持つことができる |
initialValue |
(省略可能)初回レンダー時に使用する値 |
返り値:
- 初回レンダー:
initialValueがあればそれを返す。なければvalueをそのまま返す - 更新時: まず古い値で再レンダーし、次に新しい値でバックグラウンド再レンダーを試みる
// 基本的な使い方
const deferredQuery = useDeferredValue(query);
// initialValue を指定する使い方
const deferredQuery = useDeferredValue(query, ''); // 初回は空文字
💡 古い値との比較でインジケータを表示
const isStale = query !== deferredQuery;
// isStale が true のとき、古いデータを表示中
1.4 似ているフックとの比較
useDeferredValue vs useTransition
| 項目 | useDeferredValue |
useTransition |
|---|---|---|
| 制御対象 | 値を遅延 | state 更新を遅延 |
| 使い方 | 値をラップ | 関数をラップ |
| 保留状態 | 手動で比較(value !== deferredValue) |
isPending で取得可 |
| 使用場面 | props で受け取った値を遅延させたい | 自分で state 更新を制御できる |
// useDeferredValue: 値を遅延(props から受け取った値に便利)
function ChildComponent({ searchText }) {
const deferredText = useDeferredValue(searchText);
return <SlowList text={deferredText} />;
}
// useTransition: 更新を遅延(自分で setState できる場合)
function ParentComponent() {
const [searchText, setSearchText] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
startTransition(() => {
setSearchText(e.target.value);
});
}
}
useDeferredValue vs デバウンス/スロットリング
| 項目 | useDeferredValue |
デバウンス/スロットリング |
|---|---|---|
| 遅延時間 | 動的(デバイスに適応) | 固定(例: 300ms) |
| 中断 | 可能(新しい入力で中断) | 不可 |
| 統合 | React に統合(Suspense 対応) | 外部ライブラリ必要 |
| ネットワーク | リクエスト回数削減なし | リクエスト回数削減可能 |
// useDeferredValue: デバイスの性能に応じて自動調整
const deferredQuery = useDeferredValue(query);
// デバウンス: 固定の遅延時間
const [debouncedQuery, setDebouncedQuery] = useState('');
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
}, [query]);
使い分けの指針:
- レンダーの最適化 →
useDeferredValue - ネットワークリクエストの削減 → デバウンス/スロットリング
- 両方組み合わせることも可能
1.5 重要な注意点
| ルール | 説明 |
|---|---|
| ✅ 中断可能 | 新しい値が来たらバックグラウンドレンダーを中断 |
| ✅ Suspense 連携 | サスペンド時もフォールバックを表示しない |
| ⚠️ プリミティブ値推奨 | オブジェクトはレンダー外で作成すべき |
| ⚠️ memo と組み合わせる | 子コンポーネントを memo でラップしないと効果なし |
| ❌ 固定遅延なし | 固定の遅延時間は設定できない |
// ⚠️ レンダー中に新しいオブジェクトを作成しない
function SearchPage() {
const [query, setQuery] = useState('');
// ❌ 毎回新しいオブジェクトが作成される
const deferredOptions = useDeferredValue({ query });
// ✅ プリミティブ値を使用
const deferredQuery = useDeferredValue(query);
}
// ⚠️ memo と組み合わせる
const SlowList = memo(function SlowList({ text }) {
// ...
});
2. useDeferredValue の内部構造を徹底解剖
2.0 全体像: useDeferredValue の処理フロー
🎣 useDeferredValue(フック呼び出し)
↓
📝 初回: mountDeferredValue / 更新: updateDeferredValue
↓
🔍 値が変化したか Object.is で比較
↓
⚡ 緊急更新(ユーザー入力など)の場合:
├── 古い値を返す(即座にレンダー完了)
└── DeferredLane でバックグラウンドレンダーをスケジュール
↓
🐢 遅延レンダーの場合:
└── 新しい値を返す(最終的な UI)
2.1 エントリポイント: packages/react/src/ReactHooks.js
// packages/react/src/ReactHooks.js
export function useDeferredValue<T>(value: T, initialValue?: T): T {
const dispatcher = resolveDispatcher();
return dispatcher.useDeferredValue(value, initialValue);
}
useDeferredValue が呼ばれると、現在のディスパッチャの useDeferredValue メソッドが呼び出されます。
2.2 コア実装: mountDeferredValue と updateDeferredValue
初回レンダー時の処理 (mountDeferredValue)
// packages/react-reconciler/src/ReactFiberHooks.js
function mountDeferredValue<T>(value: T, initialValue?: T): T {
const hook = mountWorkInProgressHook();
return mountDeferredValueImpl(hook, value, initialValue);
}
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
if (
// When `initialValue` is provided, we defer the initial render even if the
// current render is not synchronous.
initialValue !== undefined &&
// However, to avoid waterfalls, we do not defer if this render
// was itself spawned by an earlier useDeferredValue.
!isRenderingDeferredWork()
) {
// Render with the initial value
hook.memoizedState = initialValue;
// Schedule a deferred render to switch to the final value.
const deferredLane = requestDeferredLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane,
);
markSkippedUpdateLanes(deferredLane);
return initialValue;
} else {
hook.memoizedState = value;
return value;
}
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L2963-L3024
ポイント:
-
initialValueが指定されていれば、初回はinitialValueを返す - 同時に
DeferredLaneで再レンダーをスケジュール -
initialValueがなければvalueをそのまま返す
更新時の処理 (updateDeferredValue)
// packages/react-reconciler/src/ReactFiberHooks.js
function updateDeferredValue<T>(value: T, initialValue?: T): T {
const hook = updateWorkInProgressHook();
const resolvedCurrentHook: Hook = (currentHook: any);
const prevValue: T = resolvedCurrentHook.memoizedState;
return updateDeferredValueImpl(hook, prevValue, value, initialValue);
}
function updateDeferredValueImpl<T>(
hook: Hook,
prevValue: T,
value: T,
initialValue?: T,
): T {
if (is(value, prevValue)) {
// The incoming value is referentially identical to the currently rendered
// value, so we can bail out quickly.
return value;
} else {
// Received a new value that's different from the current value.
// Check if we're inside a hidden tree
if (isCurrentTreeHidden()) {
// Revealing a prerendered tree is considered the same as mounting new
// one, so we reuse the "mount" path in this case.
const resultValue = mountDeferredValueImpl(hook, value, initialValue);
if (!is(resultValue, prevValue)) {
markWorkInProgressReceivedUpdate();
}
return resultValue;
}
const shouldDeferValue =
!includesOnlyNonUrgentLanes(renderLanes) && !isRenderingDeferredWork();
if (shouldDeferValue) {
// This is an urgent update. Since the value has changed, keep using the
// previous value and spawn a deferred render to update it later.
// Schedule a deferred render
const deferredLane = requestDeferredLane();
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
deferredLane,
);
markSkippedUpdateLanes(deferredLane);
// Reuse the previous value.
return prevValue;
} else {
// This is not an urgent update, so we can use the latest value.
markWorkInProgressReceivedUpdate();
hook.memoizedState = value;
return value;
}
}
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L3028-L3082
ポイント:
-
Object.isで前回の値と比較 - 同じ値: そのまま返す(bailout)
-
異なる値 + 緊急更新: 古い値を返し、
DeferredLaneで再レンダーをスケジュール - 異なる値 + 非緊急更新: 新しい値を返す
2.3 遅延の判定: shouldDeferValue の条件
const shouldDeferValue =
!includesOnlyNonUrgentLanes(renderLanes) && !isRenderingDeferredWork();
| 条件 | 説明 |
|---|---|
!includesOnlyNonUrgentLanes |
現在のレンダーに緊急レーン(SyncLane, DefaultLane など)が含まれる |
!isRenderingDeferredWork |
現在が遅延レンダーではない |
つまり、緊急更新の最中で、遅延レンダーではない場合に値を遅延させます。
2.4 DeferredLane: 遅延レンダーの優先度管理
// packages/react-reconciler/src/ReactFiberLane.js
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;
DeferredLane はレーンシステムで最も低い優先度に位置します。
高優先度 ←─────────────────────────────────────────────→ 低優先度
SyncLane → DefaultLane → TransitionLanes → IdleLane → OffscreenLane → DeferredLane
2.5 requestDeferredLane: 遅延レンダーのスケジューリング
// packages/react-reconciler/src/ReactFiberWorkLoop.js
export function requestDeferredLane(): Lane {
if (workInProgressDeferredLane === NoLane) {
// If there are multiple useDeferredValue hooks in the same render, the
// tasks that they spawn should all be batched together, so they should all
// receive the same lane.
// Check the priority of the current render to decide the priority of the
// deferred task.
const isPrerendering =
includesSomeLane(workInProgressRootRenderLanes, OffscreenLane) &&
!getIsHydrating();
if (isPrerendering) {
workInProgressDeferredLane = OffscreenLane;
} else {
// Everything else is spawned as a transition.
workInProgressDeferredLane = claimNextTransitionDeferredLane();
}
}
// Mark the parent Suspense boundary so it knows to spawn the deferred lane.
const suspenseHandler = getSuspenseHandler();
if (suspenseHandler !== null) {
suspenseHandler.flags |= DidDefer;
}
return workInProgressDeferredLane;
}
引用元: packages/react-reconciler/src/ReactFiberWorkLoop.js#L852-L890
ポイント:
- 同じレンダー内の複数の
useDeferredValueは同じレーンにバッチ処理 - プリレンダリング中なら
OffscreenLaneを使用 - それ以外は
TransitionDeferredLaneを使用 - Suspense バウンダリに
DidDeferフラグをセット
2.6 isRenderingDeferredWork: 遅延レンダー中かの判定
// packages/react-reconciler/src/ReactFiberHooks.js
function isRenderingDeferredWork(): boolean {
if (!includesSomeLane(renderLanes, DeferredLane)) {
// None of the render lanes are deferred lanes.
return false;
}
// At least one of the render lanes are deferred lanes. However, if the
// current render is also batched together with an update, then we can't
// say that the render is wholly the result of deferred work.
const rootRenderLanes = getWorkInProgressRootRenderLanes();
return !includesSomeLane(rootRenderLanes, UpdateLanes);
}
引用元: packages/react-reconciler/src/ReactFiberHooks.js#L2986-L2999
これにより、遅延レンダー中は値を遅延させない(無限ループ防止)ことが保証されます。
3. ユースケース
3.1 検索結果のリスト表示
最も一般的なパターン。入力のレスポンシブ性を維持しつつ、重いリストの再レンダーを遅延。
import { useState, useDeferredValue, memo } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</>
);
}
// ⚠️ memo でラップすることが重要
const SearchResults = memo(function SearchResults({ query }) {
// 重い処理...
return <ul>{/* 結果リスト */}</ul>;
});
3.2 Suspense と組み合わせたデータフェッチ
useDeferredValue は Suspense と統合されており、ローディングフォールバックを抑制。
function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<Suspense fallback={<h2>Loading...</h2>}>
{/* 新しいクエリでサスペンドしても、
フォールバックではなく古い結果を表示 */}
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
3.3 initialValue で初期ローディングを制御
初回レンダー時のちらつきを防ぐ。
function Dashboard({ data }) {
// 初回は空配列でレンダー、すぐにバックグラウンドで本データをレンダー
const deferredData = useDeferredValue(data, []);
const isLoading = data !== deferredData;
return (
<div>
{isLoading && <Spinner />}
<Chart data={deferredData} />
</div>
);
}
3.4 props で受け取った値の遅延
子コンポーネント側で親から受け取った props を遅延させる。
// 親コンポーネント
function Parent() {
const [filter, setFilter] = useState('');
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{/* filter を直接渡す */}
<ExpensiveChild filter={filter} />
</div>
);
}
// 子コンポーネント
const ExpensiveChild = memo(function ExpensiveChild({ filter }) {
// 子側で遅延させる(親の制御不要)
const deferredFilter = useDeferredValue(filter);
return <SlowList filter={deferredFilter} />;
});
3.5 複数の値を同時に遅延
同じレンダー内の useDeferredValue はバッチ処理される。
function Dashboard({ users, products }) {
// これらは同じタイミングで更新される
const deferredUsers = useDeferredValue(users);
const deferredProducts = useDeferredValue(products);
return (
<>
<UserList users={deferredUsers} />
<ProductList products={deferredProducts} />
</>
);
}
4. トラブルシューティング
4.1 遅延が効かない(常に最新値が返る)
原因: 子コンポーネントが memo でラップされていない
// ❌ memo がないので最適化の意味がない
function SlowList({ text }) {
// ...
}
// ✅ memo でラップ
const SlowList = memo(function SlowList({ text }) {
// ...
});
4.2 毎回バックグラウンドレンダーが発生する
原因: レンダー中に新しいオブジェクトを作成している
// ❌ 毎回新しいオブジェクトが作成される
const deferredOptions = useDeferredValue({ query, filter });
// ✅ プリミティブ値を使用
const deferredQuery = useDeferredValue(query);
const deferredFilter = useDeferredValue(filter);
// ✅ または useMemo でメモ化
const options = useMemo(() => ({ query, filter }), [query, filter]);
const deferredOptions = useDeferredValue(options);
4.3 値が更新されるタイミングを制御したい
原因: useDeferredValue は固定の遅延時間を設定できない
// 固定の遅延が必要な場合はデバウンスと組み合わせる
function SearchPage() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
// デバウンスでネットワークリクエストを制御
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
}, [query]);
// useDeferredValue でレンダーを最適化
const deferredQuery = useDeferredValue(debouncedQuery);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SearchResults query={deferredQuery} />
</>
);
}
5. まとめ
useDeferredValue は React の Concurrent Features を活用した値の遅延機能を提供します。
| 特徴 | 説明 |
|---|---|
| 値の遅延 | 重いレンダーを後回しにして入力を優先 |
| 自動調整 | デバイス性能に応じて遅延時間を動的に調整 |
| 中断可能 | 新しい値が来たらバックグラウンドレンダーを中断 |
| Suspense 連携 | サスペンド時も古い値を表示し続ける |
| バッチ処理 | 同一レンダー内の複数の遅延値は同時に更新 |
使用すべき場面:
- 検索入力に連動した重いリストの表示
- props で受け取った値を遅延させたい
- Suspense と組み合わせてローディング UI を最適化
-
useTransitionが使えない場面(親から値を受け取る場合)
避けるべき場面:
-
ネットワークリクエストの回数削減(→ デバウンスを使用)
-
固定の遅延時間が必要(→ setTimeout を使用)
-
自分で state 更新を制御できる(→
useTransitionを推奨)