1
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?

React18で追加されたuseTransitionについて活用方法のまとめ

1
Posted at

はじめに

React 18で追加されたuseTransitionは、UIの応答性を保つためのフックです。

「ボタンを押したのに画面が固まる」「フィルターを切り替えたら一瞬フリーズする」こうした体験を改善できます。

この記事では、useTransitionの基本的な仕組みから、Next.js App Routerでの使い方をメモとして残します。

対象読者

  • Reactの基本的なフック(useState, useEffect)を使ったことがある方
  • useTransitionを聞いたことはあるけど使ったことがない方
  • Next.js App Routerを使っている方

環境

  • React 18以降
  • Next.js 14(App Router)
  • TypeScript

useTransitionの基本

stateの更新には優先度がある

通常、Reactではstateの更新はすべて同じ優先度で処理されます。例えば、ボタンをクリックして2つのstateを更新する場合、Reactは両方の更新が完了するまでUIを再レンダリングしません。

ここで問題になるのが、重い処理を含むstate更新です。

const handleClick = () => {
  setInputValue(value);       // 軽い処理(入力欄の更新)
  setFilteredList(heavyCalc(value)); // 重い処理(大量データのフィルタリング)
};

この場合、heavyCalcが完了するまで入力欄の更新も反映されず、ユーザーには「フリーズした」ように見えます。

useTransitionで優先度を分ける

useTransitionを使うと、state更新を「緊急(urgent)」と「非緊急(transition)」に分けられます。

import { useState, useTransition } from 'react';

const [isPending, startTransition] = useTransition();

useTransitionは配列で2つの値を返します。

第1引数:isPending(boolean)

startTransitionで包んだ更新が処理中かどうかを示すフラグです。
トランジション中はtrueになり、完了するとfalseに戻ります。
この値を使ってローディング表示やボタンの非活性化ができます。

第2引数:startTransition(関数)

state更新を「非緊急」としてマークするための関数です。
この関数に渡したコールバックの中にstate更新を書くと、Reactはそれを後回しにして、他の緊急な更新を先に処理します。

第1引数のisPendingで「今トランジション中?」を判定し、第2引数のstartTransitionで「この更新は後回しにしていいよ」とReactに伝える、という役割分担です。

const handleClick = () => {
  setInputValue(value);              // 緊急: すぐに反映される

  startTransition(() => {
    setFilteredList(heavyCalc(value)); // 非緊急: 後回しにしてOK
  });
};

こうすると、入力欄は即座に更新され、フィルタリング結果は裏側で処理されます。処理中はisPendingtrueになるので、ローディング表示を出すこともできます。

シンプルな使用例

タブの切り替えで大量のリストをフィルタリングするケースを考えてみましょう。

useTransitionなしの場合

FilterTabs.tsx
'use client';

import { useState } from 'react';

export default function FilterTabs({ items }: { items: Item[] }) {
  const [tab, setTab] = useState<'all' | 'active'>('all');
  const [filteredItems, setFilteredItems] = useState(items);

  const handleTabChange = (newTab: 'all' | 'active') => {
    setTab(newTab);
    // 大量データのフィルタリング(重い処理)
    const result = items.filter((item) =>
      newTab === 'all' ? true : item.isActive
    );
    setFilteredItems(result);
  };

  return (
    <div>
      <button onClick={() => handleTabChange('all')}>すべて</button>
      <button onClick={() => handleTabChange('active')}>アクティブ</button>
      {/* filteredItemsの描画... */}
    </div>
  );
}

itemsが数千件あると、タブを押してからUIが反映されるまでにラグが出ます。

useTransitionありの場合

FilterTabs.tsx
'use client';

import { useState, useTransition } from 'react';

export default function FilterTabs({ items }: { items: Item[] }) {
  const [tab, setTab] = useState<'all' | 'active'>('all');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleTabChange = (newTab: 'all' | 'active') => {
    setTab(newTab); // 緊急: タブのハイライトはすぐ切り替わる

    startTransition(() => {
      // 非緊急: フィルタリング結果は後から反映される
      const result = items.filter((item) =>
        newTab === 'all' ? true : item.isActive
      );
      setFilteredItems(result);
    });
  };

  return (
    <div>
      <button onClick={() => handleTabChange('all')}>すべて</button>
      <button onClick={() => handleTabChange('active')}>アクティブ</button>
      {isPending && <p>読み込み中...</p>}
      {/* filteredItemsの描画... */}
    </div>
  );
}

変更点は3つだけです。

  1. useTransition()を呼ぶ
  2. 重い更新をstartTransition()で包む
  3. isPendingでローディング表示を出す

これでタブのハイライトは即座に切り替わり、リストの更新は裏側で行われるようになります。

Next.js App Routerのrouter操作で使う

なぜrouter操作でUIが固まるのか

App Routerでは、router.push()router.replace()でURLパラメータを変更すると、サーバーコンポーネントの再フェッチが走ります。

ユーザーがフィルターをクリック
  → router.replace('?productId=1')
  → サーバーコンポーネントがAPIを叩いてデータ再取得
  → データが返ってくるまでUIが固まる 😱

この間、クライアント側のstate更新もブロックされるため、ローディング表示すら出せません。

startTransitionで包む

router.replace()startTransitionで包むと、この問題が解決します。

MaterialContainer.tsx
'use client';

import { useRouter, useSearchParams } from 'next/navigation';
import { useTransition } from 'react';

// URLパラメータの更新が完了するまでの間、isPendingがtrueになる
const [isPending, startTransition] = useTransition();

const handleItemFilterToggle = (productId: number) => {
  const params = new URLSearchParams(searchParams);

  // 緊急: ローディング状態を即座に設定
  setLoadingForProductId(newProductId);

  // 非緊急: router.replace() をトランジションとしてマーク
  startTransition(() => {
    params.set('productId', productId.toString());
    router.replace(`?${params.toString()}`, { scroll: false });
  });
};

こうすることで以下の流れになります。

ユーザーがフィルターをクリック
  → ローディング表示が即座に出る ✅
  → router.replace()が非緊急として実行される
  → サーバーコンポーネントの再フェッチが裏側で進む
  → データが返ってきたらUIが更新される

isPendingでローディングを出す

isPendingを使えば、サーバーコンポーネントの再フェッチ中にローディング表示を出せます。

{/* フィルターボタンをpending中は非活性にする */}
<button
  type='button'
  onClick={() => setIsItemFilterOpen(true)}
  disabled={isLoading || isPending}
>
  つくりたいアイテムで絞り込む
</button>

{/* pending中はローディング表示 */}
{isPending && <CommonLoading />}

ポイントは、isPendingを活用してUIのフィードバックを充実させることです。

  • ボタンをdisabledにして二重クリックを防ぐ
  • ローディングスピナーを表示して「処理中」と伝える
  • コンテンツ領域を非表示にしてチラつきを防ぐ

useTransitionを使うべきタイミング

すべてのstate更新にuseTransitionを使う必要はありません。以下のケースで効果を発揮します。

使うべき場面

ケース 具体例
重いstate更新がUIをブロックする 大量データのフィルタリング、ソート
router操作でUIが固まる router.push(), router.replace()での画面遷移・パラメータ更新
ユーザー入力の応答性を保ちたい 検索インクリメンタルサーチ

使わなくてよい場面

ケース 理由
軽いstate更新 そもそもブロックが発生しない
入力フォームのバリデーション 入力値そのものは緊急更新すべき
APIのfetch完了待ち useTransitionは非同期処理を待つものではない(※後述)

注意点:startTransitionの中にはstate更新を入れる

startTransitionはstate更新をトランジションとしてマークするものです。awaitやPromiseを直接入れても意味がありません。

// ❌ こうではなく
startTransition(async () => {
  const data = await fetchData();
  setItems(data);
});

// ✅ こう使う(state更新だけを包む)
const data = await fetchData();
startTransition(() => {
  setItems(data);
});

まとめ

  • useTransitionは、state更新を「緊急」と「非緊急」に分けるフック
  • startTransitionで非緊急の更新を包み、isPendingでローディング表示を出す
  • Next.js App Routerではrouter.replace()startTransitionで包むことで、URLパラメータ更新中もUIの応答性を保てる

参考

1
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
1
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?