0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Qiita Advent Calender 2025企画】React Hooks を Hackしよう!【Part14: useDeferredValueをふかぼろう!】

Posted at

useDeferredValueUI の一部の更新を遅延させるための 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);
}

引用元: packages/react/src/ReactHooks.js#L178-L181

useDeferredValue が呼ばれると、現在のディスパッチャの useDeferredValue メソッドが呼び出されます。

2.2 コア実装: mountDeferredValueupdateDeferredValue

初回レンダー時の処理 (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

ポイント:

  1. initialValue が指定されていれば、初回は initialValue を返す
  2. 同時に DeferredLane で再レンダーをスケジュール
  3. 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

ポイント:

  1. Object.is で前回の値と比較
  2. 同じ値: そのまま返す(bailout)
  3. 異なる値 + 緊急更新: 古い値を返し、DeferredLane で再レンダーをスケジュール
  4. 異なる値 + 非緊急更新: 新しい値を返す

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;

引用元: packages/react-reconciler/src/ReactFiberLane.js#L109

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

ポイント:

  1. 同じレンダー内の複数の useDeferredValue同じレーンにバッチ処理
  2. プリレンダリング中なら OffscreenLane を使用
  3. それ以外は TransitionDeferredLane を使用
  4. 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 が使えない場面(親から値を受け取る場合)

避けるべき場面:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?