useTransitionとuseDeferredValueは、どちらも「重い画面更新を後回しにして、入力などの急ぎの更新を先に通す」ためのReactのフックです。違いは後回しにする対象で、useTransitionは「stateを更新する処理」を、useDeferredValueは「受け取った値」を後回しにします。 stateの更新を自分で書ける場所ではuseTransition、propsやカスタムフックから値だけを受け取る場所ではuseDeferredValueを選びます。

この記事では、1万件のリストの絞り込みを「何もしない/useTransition/useDeferredValue/debounce」の4通りで書き、入力欄に文字が反映されるまでの時間、リストの描画回数、isPending の変化を実際に計測して比べます。 さらに、「入力欄のstateには使えない」「await のあとの更新はもう一度 startTransition で包む」「フックなしの startTransition との違い」などの制約を、うまく動かないコードと直したコードの対で、出力つきで示します。

確認環境:Ubuntu 24.04、Node.js 22.22.2、React 19.3.0 / react-dom 19.3.0。esbuild 0.28.2でまとめ、Playwright 1.56.0のChromium 141で動かしました。4通りの比較は本番ビルド、制約の確認は開発ビルドです。

useTransitionとuseDeferredValueの早見表

項目 useTransition useDeferredValue
後回しにするもの stateを更新する処理(setState の呼び出し) 値(propsやstate)
書き方 const [isPending, startTransition] = useTransition() const deferred = useDeferredValue(value)
使える条件 更新する set 関数を自分で呼べる 値を受け取れればよい
待ち状態の判定 isPending が用意されている value !== deferred を自分で比べる
非同期処理(React 19) startTransition(async () => { ... }) と書ける 対象外
重い子コンポーネント そのままでも効く memo で包む必要がある
入力欄の value に使うstate 向かない(別のstateに分ける) 入力欄は元の値、リストは遅延した値を使う
向いている場面 タブの切り替え、絞り込み、保存ボタンの処理 親やライブラリから渡された値で重い描画をする
やりたいこと 選ぶもの
自分で書いた setState を後回しにしたい useTransition
propsやカスタムフックの戻り値で重い描画をしている useDeferredValue
更新中の表示(スピナーや薄い表示)を出したい useTransitionの isPending、またはuseDeferredValueの値の比較
コンポーネントの外(ルーターやデータ取得の層)から更新を始めたい フックなしの startTransition(react からimport)
APIへのリクエスト回数を減らしたい debounce(描画の後回しとは目的が別)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「useTransitionとuseDeferredValueの違いと使い方」

1万件リストの絞り込みを4通りで書く

共通部分:データ、リスト、計測

1万件の文字列を用意し、入力した文字を含むものだけを表示します。行ごとに少し計算がある画面を想定して、Row に軽い計算を入れてあります(この環境では1万行のレンダーに約400ミリ秒かかります)。

// t1.jsx(React 19.3.0)共通部分
import { memo, useDeferredValue, useEffect, useLayoutEffect, useRef, useState, useTransition } from 'react';
import { createRoot } from 'react-dom/client';

// 1万件のデータ
const ITEMS = Array.from({ length: 10000 }, (_, i) => `item-${String(i).padStart(4, '0')}`);

// 計測用のカウンター
const stat = { listRender: 0, listCommit: 0, rowRender: 0, inputLag: [], pending: [] };
window.stat = stat;

// 1行ぶんの表示。行ごとに少し計算がある想定で、軽い計算を入れてある
function Row({ text }) {
  stat.rowRender++;
  let x = 0;
  for (let i = 0; i < 20000; i++) x += Math.sqrt(i);
  return <li data-x={x > 0 ? 1 : 0}>{text}</li>;
}

// 絞り込み結果のリスト。query が同じなら再レンダーしない
const List = memo(function List({ query }) {
  stat.listRender++;
  const rows = ITEMS.filter((t) => t.includes(query));
  useEffect(() => { stat.listCommit++; });
  return (
    <ul id="list" data-query={query}>
      {rows.map((t) => <Row key={t} text={t} />)}
    </ul>
  );
});

// 入力してから入力欄に反映されるまでの時間を測る
function useInputLag(text) {
  const t0 = useRef(0);
  useLayoutEffect(() => {
    if (t0.current) stat.inputLag.push(Math.round(performance.now() - t0.current));
  }, [text]);
  return () => { t0.current = performance.now(); };
}

計測する項目は次のとおりです。

項目 意味
リストのレンダー開始 List の関数が呼ばれた回数。途中でやめたレンダーも数える
画面への反映 List のレンダーが最後まで終わり、画面に出た回数
行のレンダー Row の関数が呼ばれた合計回数
入力欄の反映 キー入力から、入力欄に文字が反映されるまでの時間
完了まで 最初のキー入力から、最終的なリストが表示されるまでの時間

4通りの書き方

// ① 何もしない:入力とリストが同じ state
function Plain() {
  const [text, setText] = useState('');
  const mark = useInputLag(text);
  return (
    <>
      <input id="q" value={text} onChange={(e) => { mark(); setText(e.target.value); }} />
      <List query={text} />
    </>
  );
}
// ② useTransition:リスト用の state の更新を startTransition で包む
function WithTransition() {
  const [text, setText] = useState('');
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const mark = useInputLag(text);
  useEffect(() => { stat.pending.push(isPending); }, [isPending]);
  return (
    <>
      <input id="q" value={text} onChange={(e) => {
        mark();
        setText(e.target.value);                              // 入力欄はすぐ更新
        startTransition(() => setQuery(e.target.value));      // リストは後回しでよい
      }} />
      {isPending && <span id="pending">更新中…</span>}
      <List query={query} />
    </>
  );
}
// ③ useDeferredValue:値の「遅れてついてくる版」をリストに渡す
function WithDeferred() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  const isStale = text !== deferredText;
  const mark = useInputLag(text);
  useEffect(() => { stat.pending.push(isStale); }, [isStale]);
  return (
    <>
      <input id="q" value={text} onChange={(e) => { mark(); setText(e.target.value); }} />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <List query={deferredText} />
      </div>
    </>
  );
}
// ④ debounce:入力が 300ms 止まってからリスト用の state を更新する
function WithDebounce() {
  const [text, setText] = useState('');
  const [query, setQuery] = useState('');
  const mark = useInputLag(text);
  useEffect(() => {
    const id = setTimeout(() => setQuery(text), 300);
    return () => clearTimeout(id);
  }, [text]);
  return (
    <>
      <input id="q" value={text} onChange={(e) => { mark(); setText(e.target.value); }} />
      <List query={query} />
    </>
  );
}

4つのうち1つを選んで描画します。

const variants = { plain: Plain, transition: WithTransition, deferred: WithDeferred, debounce: WithDebounce };
const App = variants[window.VARIANT];
createRoot(document.getElementById('root')).render(<App />);

計測結果:「item」と100ミリ秒間隔で4文字入力する

Playwrightで入力欄に item と1文字ずつ、100ミリ秒間隔で入力しました。4文字とも1万件すべてに一致するので、文字を打つたびに1万行の再描画が必要になる条件です。pending は、useTransitionでは isPending、useDeferredValueでは text !== deferredText の変化を記録したものです。

plain      | リストのレンダー開始 4 回 | 画面への反映 4 回 | 行のレンダー 40000 回 | 入力欄の反映 [402,419,391,383] ms | 完了まで 2040 ms | 入力欄 "item"
transition | リストのレンダー開始 4 回 | 画面への反映 1 回 | 行のレンダー 16702 回 | 入力欄の反映 [4,1,0,0] ms | 完了まで 787 ms | 入力欄 "item" | pending: [true,false]
deferred   | リストのレンダー開始 4 回 | 画面への反映 1 回 | 行のレンダー 17623 回 | 入力欄の反映 [2,0,0,0] ms | 完了まで 770 ms | 入力欄 "item" | pending: [true,false]
debounce   | リストのレンダー開始 1 回 | 画面への反映 1 回 | 行のレンダー 10000 回 | 入力欄の反映 [2,0,0,0] ms | 完了まで 1036 ms | 入力欄 "item"

3回実行した範囲をまとめると、次のようになります。

書き方 入力欄の反映 リストのレンダー開始 画面への反映 行のレンダー 完了まで
何もしない 380〜442ミリ秒 4回 4回 40,000回 1,989〜2,073ミリ秒
useTransition 0〜4ミリ秒 4回 1回 16,227〜16,702回 787〜815ミリ秒
useDeferredValue 0〜2ミリ秒 4回 1回 15,849〜17,623回 770〜778ミリ秒
debounce(300ミリ秒) 0〜3ミリ秒 1回 1回 10,000回 1,036〜1,081ミリ秒

結果の読み方

  • 何もしない場合、1文字ごとに入力欄の反映が約400ミリ秒待たされます。 入力欄とリストが同じ更新に入っているので、1万行のレンダーが終わるまで入力欄も変わりません。4文字ぶんのレンダーをすべて最後まで行うため、完了までに約2秒かかりました。
  • useTransitionとuseDeferredValueでは、入力欄の反映が数ミリ秒になります。 リストのレンダーは4回始まっていますが、画面に反映されたのは最後の1回だけです。次のキー入力が来た時点で、途中のレンダーをやめて入力を先に処理し、新しい値でレンダーをやり直しています。行のレンダーが40,000回ではなく約16,000〜17,600回で済んでいるのは、途中でやめた分があるためです。
  • isPending は、入力の最初に true になり、最後のリストが表示された時点で false に戻ります([true,false])。1文字ごとに行き来はせず、入力が続いている間は true のままです。useDeferredValueの「値の比較」も同じ動きでした。
  • debounceは、入力が300ミリ秒止まるまでリストを更新しません。 レンダーは1回で済みますが、完了までの時間には待ち時間の300ミリ秒が必ず入ります。今回は入力が終わったあとにリストのレンダーが始まったので、入力欄の反映は待たされていません。

公式リファレンスのuseDeferredValueのページにも、debounceやthrottleとの違いとして、固定の待ち時間を選ばなくてよいこと、端末が速ければすぐに、遅ければその分だけ遅れて再描画されること、遅延した再描画は途中で中断できること、debounceやthrottleはレンダーが入力を待たせる瞬間を先送りするものであることが書かれています。一方、レンダー以外の仕事(ネットワークへのリクエスト回数を減らすなど)にはdebounceやthrottleが引き続き役立ち、組み合わせて使えるとも書かれています。

useTransitionとuseDeferredValueの選び方

計測結果のとおり、画面の動きとしては2つはほぼ同じです。選び方は「後回しにしたい更新を、自分のコードで書いているかどうか」で決まります。

状況 選ぶもの 理由
setQuery や setTab を自分のイベントハンドラーで呼んでいる useTransition その呼び出しを startTransition で包めばよい
値がpropsで渡ってくる(更新しているのは親) useDeferredValue set 関数に手が届かない
ライブラリのフックが返す値で重い描画をしている useDeferredValue 更新の場所がライブラリの中にある
更新中の表示を簡単に出したい useTransition isPending をそのまま使える
ボタンを押して保存や取得をする(非同期) useTransition React 19では async 関数を渡せる
入力欄を1つのstateで済ませたい useDeferredValue stateを2つに分けずに書ける

公式リファレンスのuseTransitionの注意事項にも、トランジションで包めるのはそのstateの set 関数を呼べる場合だけで、propsやカスタムフックの値に応じて始めたいときはuseDeferredValueを試すように書かれています。

2つの書き方を並べると、形の違いがはっきりします。

手順 useTransition useDeferredValue
stateの数 2つ(入力用の text、リスト用の query) 1つ(text)
後回しの指定 startTransition(() => setQuery(v)) const deferredText = useDeferredValue(text)
リストに渡す値 query deferredText
更新中の判定 isPending text !== deferredText

useDeferredValueの第2引数(初期値)

React 19では、useDeferredValueに第2引数で初期値を渡せます。最初のレンダーでは初期値が返り、そのあと本来の値でのレンダーが後回しで行われます。

// D:useDeferredValue の第2引数(初期値)
function Initial() {
  const deferred = useDeferredValue('本来の値', '初期値');
  console.log('render: deferred =', deferred);
  return <p>{deferred}</p>;
}
render: deferred = 初期値
render: deferred = 本来の値

第2引数を省いた場合、最初のレンダーでは遅延は起きません。公式リファレンスには、代わりに描画できる「前の値」が無いためと説明されています。

制約と直し方:うまく動かないコードと直したコードの対

入力欄のvalueに使うstateはトランジションで更新しない

入力欄の value に渡しているstateを startTransition の中で更新すると、入力した文字が入力欄に残りません。

// ⑤ 向かない例:入力欄の state そのものをトランジションで更新する
function InputInTransition() {
  const [text, setText] = useState('');
  const [isPending, startTransition] = useTransition();
  return (
    <>
      <input id="q" value={text} onChange={(e) => {
        const v = e.target.value;
        stat.pending.push('入力イベント:' + v);
        startTransition(() => setText(v));
      }} />
      <List query={text} />
    </>
  );
}

item と入力した結果です。

badinput   | リストのレンダー開始 4 回 | 画面への反映 1 回 | 行のレンダー 16336 回 | 入力欄の反映 [] ms | 完了まで 15456 ms | 入力欄 "m" | pending: ["入力イベント:i","入力イベント:t","入力イベント:e","入力イベント:m"]

入力欄に残ったのは最後の1文字 m だけです。 入力イベントで届いた値が i、t、e、m と1文字ずつになっていることから、キーを打つたびに入力欄が空に戻っていたことがわかります。「完了まで」が約15秒になっているのは、リストが item で絞り込まれた状態にならず、計測の待ち時間の上限に達したためです。

入力欄は、打った文字をその場で反映する必要があります。公式リファレンスにも、トランジションの更新は入力欄の制御には使えないと書かれています。直し方は、stateを2つに分けることです。 入力欄用のstateはふつうに更新し、リスト用のstateだけを startTransition で包みます。これは最初に示した WithTransition の形そのものです。

<input value={text} onChange={(e) => {
  setText(e.target.value);                          // 入力欄はすぐ更新
  startTransition(() => setQuery(e.target.value));  // リストは後回しでよい
}} />

stateを分けたくない場合は、useDeferredValueで text の遅延版を作ってリストに渡します。

awaitのあとの更新は、もう一度startTransitionで包む

React 19では、startTransition に async 関数を渡せます。関数が終わるまで isPending が true になるので、送信中の表示を自分で管理しなくて済みます。ただし、await のあとに書いたstate更新は、そのままではトランジションとして扱われません。

画面に反映されるたびに isPending と data を記録するフックを用意して、2つの書き方を比べます。

import { Component, startTransition as standaloneStartTransition, useDeferredValue, useEffect, useState, useTransition } from 'react';
import { createRoot } from 'react-dom/client';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = (...a) => console.log(...a);

// 画面に反映されるたびに isPending と data を記録する
function useCommitLog(isPending, data) {
  useEffect(() => { log(`画面: isPending=${isPending} data=${data}`); });
}
// A1:await のあとの更新をそのまま書く
function AwaitPlain() {
  const [data, setData] = useState('以前の値');
  const [isPending, startTransition] = useTransition();
  useCommitLog(isPending, data);
  return <button id="go" onClick={() => {
    startTransition(async () => {
      await sleep(200);
      setData('新しい値');
    });
  }}>読み込む</button>;
}
画面: isPending=false data=以前の値
--- クリック
画面: isPending=true data=以前の値
画面: isPending=true data=新しい値
画面: isPending=false data=新しい値

3行目に注目してください。isPending が true のまま、data だけが先に新しい値に変わっています。 await のあとの setData が、トランジションとは別の「急ぎの更新」として処理されたためです。更新中の表示と新しいデータが同時に出る瞬間ができ、重い描画であれば入力を待たせることにもなります。

// A2:await のあとの更新を、もう一度 startTransition で包む
function AwaitWrapped() {
  const [data, setData] = useState('以前の値');
  const [isPending, startTransition] = useTransition();
  useCommitLog(isPending, data);
  return <button id="go" onClick={() => {
    startTransition(async () => {
      await sleep(200);
      startTransition(() => setData('新しい値'));
    });
  }}>読み込む</button>;
}
画面: isPending=false data=以前の値
--- クリック
画面: isPending=true data=以前の値
画面: isPending=false data=新しい値

包み直すと、isPending が false に戻るのと新しい値の表示が、同じ1回の反映にまとまります。 公式リファレンスには、これは既知の制限で、将来直す予定だと書かれています。

startTransitionに渡す関数の中で、同期的に更新する

startTransition に渡した関数は、その場ですぐに実行されます。トランジションとして扱われるのは、その関数が動いている間に呼ばれたstate更新です。setTimeout の中に書いた更新は、関数が終わったあとに実行されるので対象になりません。

// B1:startTransition の中で setTimeout を使う
function TimeoutInside() {
  const [data, setData] = useState('以前の値');
  const [isPending, startTransition] = useTransition();
  useCommitLog(isPending, data);
  return <button id="go" onClick={() => {
    startTransition(() => {
      setTimeout(() => setData('新しい値'), 200);
    });
  }}>読み込む</button>;
}
画面: isPending=false data=以前の値
--- クリック
画面: isPending=true data=以前の値
画面: isPending=false data=以前の値
画面: isPending=false data=新しい値

isPending は一度 true になりますが、データが変わる前に false へ戻っています。 トランジションの中に更新が1つも無かったためです。その後の setData は、ふつうの更新として処理されています。

// B2:setTimeout の中で startTransition を呼ぶ
function TimeoutOutside() {
  const [data, setData] = useState('以前の値');
  const [isPending, startTransition] = useTransition();
  useCommitLog(isPending, data);
  return <button id="go" onClick={() => {
    setTimeout(() => {
      startTransition(() => setData('新しい値'));
    }, 200);
  }}>読み込む</button>;
}
画面: isPending=false data=以前の値
--- クリック
画面: isPending=true data=以前の値
画面: isPending=false data=新しい値

setTimeout のコールバックの中で startTransition を呼ぶと、更新がトランジションになり、isPending と新しい値の切り替わりがそろいます。

コンポーネントの外では、フックなしのstartTransitionを使う

useTransitionはフックなので、コンポーネントかカスタムフックの中でしか呼べません。ルーターやデータ取得の層のように、コンポーネントの外から更新を始めたいときは、react からimportする startTransition を使います。

// E:コンポーネントの外(ルーターやデータ取得の層など)から更新を始める
let setPageFromOutside = null;
// E1:ふつうの関数の中で useTransition を呼ぶ
function navigateWithHook(page) {
  const [, startTransition] = useTransition();
  startTransition(() => setPageFromOutside(page));
}
// E2:フックなしの startTransition を使う
function navigate(page) {
  standaloneStartTransition(() => setPageFromOutside(page));
}
function Outside() {
  const [page, setPage] = useState('/home');
  setPageFromOutside = setPage;
  useEffect(() => { log(`画面: page=${page}`); });
  return (
    <>
      <button id="go" onClick={() => {
        try { navigateWithHook('/about'); } catch (e) { log('E1 のエラー:', e.message.split('\n')[0]); }
        navigate('/about');
      }}>移動</button>
      <p>{page}</p>
    </>
  );
}
画面: page=/home
--- クリック
E1 のエラー: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
画面: page=/about

ふつうの関数の中でuseTransitionを呼ぶと Invalid hook call になり、フックなしの startTransition では画面が /about に切り替わりました。

項目 useTransition フックなしのstartTransition
import import { useTransition } from 'react' import { startTransition } from 'react'
呼べる場所 コンポーネント、カスタムフックの中 どこでも
isPending ある ない
エラーの行き先 エラーバウンダリ window の error イベント(reportError)

エラー処理:エラーバウンダリで受ける

useTransitionの startTransition に渡した関数がエラーを投げた(または拒否されたPromiseを返した)場合、そのコンポーネントを囲むエラーバウンダリに届きます。

// エラーバウンダリ
class ErrorBoundary extends Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }
  render() {
    if (this.state.error) return <p id="fallback">エラー表示: {this.state.error.message}</p>;
    return this.props.children;
  }
}
// C1:useTransition のアクションでエラーが起きる
function ActionError() {
  const [isPending, startTransition] = useTransition();
  return <button id="go" onClick={() => {
    startTransition(async () => {
      await sleep(100);
      throw new Error('保存できませんでした');
    });
  }}>{isPending ? '保存中…' : '保存'}</button>;
}

ErrorBoundary で囲んで描画し、ボタンを押した結果です(onCaughtError は createRoot のオプションで、エラーバウンダリが受けたエラーを知らせるコールバックです)。

--- クリック
onCaughtError: 保存できませんでした
最終の画面: エラー表示: 保存できませんでした

同じ処理をフックなしの startTransition で書くと、エラーバウンダリには届きません。

// C2:フックなしの startTransition でエラーが起きる
function StandaloneError() {
  return <button id="go" onClick={() => {
    standaloneStartTransition(async () => {
      await sleep(100);
      throw new Error('保存できませんでした');
    });
  }}>保存</button>;
}
--- クリック
window の error イベント: Uncaught Error: 保存できませんでした
最終の画面: 保存

画面はボタンのままで、エラーは window の error イベントとして報告されました。 公式リファレンスにも、フックなしの関数はコンポーネントにひも付いていないため、エラーは reportError で報告されると書かれています。利用者にエラーを見せたい処理は、useTransitionとエラーバウンダリの組み合わせにします。

useDeferredValueは、重い子をmemoで包む

useDeferredValueで遅延した値を渡しても、子コンポーネントが memo で包まれていないと効果が出ません。

// ⑥ 向かない例:memo で包んでいないリストに useDeferredValue の値を渡す
function ListNoMemo({ query }) {
  stat.listRender++;
  const rows = ITEMS.filter((t) => t.includes(query));
  useEffect(() => { stat.listCommit++; });
  return <ul id="list" data-query={query}>{rows.map((t) => <Row key={t} text={t} />)}</ul>;
}
function DeferredNoMemo() {
  const [text, setText] = useState('');
  const deferredText = useDeferredValue(text);
  const mark = useInputLag(text);
  return (
    <>
      <input id="q" value={text} onChange={(e) => { mark(); setText(e.target.value); }} />
      <ListNoMemo query={deferredText} />
    </>
  );
}
nomemo     | リストのレンダー開始 8 回 | 画面への反映 5 回 | 行のレンダー 57503 回 | 入力欄の反映 [408,378,414,398] ms | 完了まで 2363 ms | 入力欄 "item"

入力欄の反映は約400ミリ秒に戻り、リストのレンダー開始は8回に増えました。 文字を打つと、まず「入力欄は新しい値、リストは前の値」で親がレンダーされます。memo が無いと、値が変わっていないリストもこの時点でレンダーされ直すので、入力欄の反映が待たされます。memo で包んだ List に変えると、最初の比較の「deferred」の行のとおり、入力欄の反映は0〜2ミリ秒になります。

制約のまとめ

うまく動かない書き方 起きること 直し方
入力欄の value のstateを startTransition で更新 入力した文字が残らない(item と打って m だけ残った) 入力用とリスト用のstateを分ける。またはuseDeferredValue
await のあとに setState をそのまま書く isPending が true のまま新しい値が先に出る await のあとでもう一度 startTransition で包む
startTransition の中の setTimeout で更新 isPending がすぐ false に戻り、更新はふつうの更新になる setTimeout のコールバックの中で startTransition を呼ぶ
コンポーネントの外でuseTransitionを呼ぶ Invalid hook call フックなしの startTransition を使う
フックなしの startTransition でエラーを画面に出したい エラーバウンダリに届かない useTransitionを使い、エラーバウンダリで囲む
useDeferredValueの値を memo なしの子に渡す 入力欄の反映が待たされる 子を memo で包む

よくある質問

Q. useTransitionとuseDeferredValueは、どちらが速いですか? A. 今回の計測では、入力欄の反映(0〜4ミリ秒と0〜2ミリ秒)も、完了までの時間(787〜815ミリ秒と770〜778ミリ秒)もほぼ同じでした。速さではなく、「set 関数を自分で呼べるならuseTransition、値だけ受け取るならuseDeferredValue」で選びます。

Q. debounceはもう要りませんか? A. 目的が違うので、引き続き使います。useTransitionとuseDeferredValueは「レンダーを後回しにして、途中でやめられるようにする」仕組みで、リクエストの回数は減らしません。入力のたびにAPIを呼ぶ処理は、debounceで回数を抑えます。画面の描画が重い場合は、両方を組み合わせられます。

Q. isPending が true の間、画面には何が表示されていますか? A. 更新前の内容がそのまま表示されています。今回の例では、入力中は前のリストが出たままで、最後のレンダーが終わった時点で新しいリストに切り替わりました。isPending を使って「更新中…」と出したり、リストを薄く表示したりすると、処理が進んでいることを伝えられます。

Q. トランジションにすると、レンダーそのものは速くなりますか? A. なりません。1万行のレンダーにかかる時間は同じです。変わるのは優先度で、入力などの急ぎの更新が来たら途中のレンダーをやめて先に通します。レンダー自体を軽くしたい場合は、表示する行数を絞る(ページ分けや仮想スクロール)などの対策を合わせて行います。

まとめ

  • useTransitionは「stateを更新する処理」を、useDeferredValueは「値」を後回しにする。 set 関数を自分で呼べるならuseTransition、値だけ受け取るならuseDeferredValue。
  • 1万件の絞り込みで、入力欄の反映は約400ミリ秒から数ミリ秒になった。 リストのレンダーは4回始まり、画面に反映されたのは最後の1回だけだった。
  • isPending は入力の最初に true、最後のリストが出た時点で false。 入力が続く間は true のまま。
  • debounceはレンダーが1回で済むが、待ち時間が必ず入る。 リクエスト回数を減らす目的では引き続き役立つ。
  • 入力欄の value に使うstateはトランジションで更新しない。 入力用とリスト用のstateを分ける。
  • await のあとや setTimeout の中の更新は、その場で startTransition で包む。 包まないと isPending と表示の切り替わりがずれる。
  • コンポーネントの外ではフックなしの startTransition。 isPending は無く、エラーはエラーバウンダリに届かない。useDeferredValueを使うときは重い子を memo で包む。

参考・出典

  • React 公式リファレンス「useTransition」:https://react.dev/reference/react/useTransition
  • React 公式リファレンス「useDeferredValue」:https://react.dev/reference/react/useDeferredValue
  • React 公式リファレンス「startTransition」:https://react.dev/reference/react/startTransition

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「useTransitionとuseDeferredValueの違いと使い方」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?