T-CREATOR

Suspense + useTransitionで滑らかなUXを実現するやり方を紹介

Suspense + useTransitionで滑らかなUXを実現するやり方を紹介

ReactアプリのUXを一段階進化させるなら、この組み合わせが鍵となります。

SuspenseuseTransition を活用することで、重たいデータ読み込みや画面遷移時にも、ユーザーにストレスを与えない滑らかな操作体験を提供できます。

この記事では、それぞれの役割を丁寧に解説し、実際にアプリに導入する手順と注意点を、豊富なサンプルコードとともにお届けいたします。

なぜ滑らかなUXが求められるのか

ユーザーは単に「使える」アプリケーションを求めているのではありません。
違和感のない、気持ちよく動くインターフェースを求めています。

特にReactのようなコンポーネントベースのUIでは、非同期データの扱い再描画のタイミングによって体験が大きく左右されます。

次のような“スムーズさ”が、ユーザーの満足度を大きく左右します:

シチュエーション良いUX(スムーズ)悪いUX(ストレス)
検索フィルター入力と同時に候補表示。遅延は気にならない入力ごとにガクつき。リストがフラッシュ表示
ページ遷移現在の状態を保ったまま、新画面がふわっと現れる白い画面のあとにコンテンツがカクッと表示
API取得中のボタンボタンがグレーアウトしローディングが見える押したあと何も反応がなく、不安になる

この「体感レスポンスの違い」は、実際の処理速度よりも演出や心理設計の巧みさに影響されることが多いのです。

UX向上の鍵を握る2つのAPI

React 18では、こうした課題に対応するために次の2つの仕組みが登場しました。

  • Suspense: 非同期UIの構成とフォールバック表示の制御
  • useTransition: UI更新の優先度を調整して即時応答性とバックグラウンド処理の両立を可能に

この2つは、別々に使うこともできますが、組み合わせることで真価を発揮します。

実例:フィルター付きリストの反応性を改善

ここでは、ユーザーが検索バーに入力するたびにリストをフィルタリングするケースを取り上げます。
useTransition を使わない場合、処理が重ければ重いほど UIが固まり、タイピングに“引っかかり”が出ます。

before: useTransitionなし(遅延が気になる)

tsxfunction FilterList({ allItems }) {
  const [input, setInput] = useState('');
  const [filtered, setFiltered] = useState(allItems);

  const onChange = (e) => {
    const value = e.target.value;
    setInput(value);

    const filtered = allItems.filter(item =>
      item.toLowerCase().includes(value.toLowerCase())
    );
    setFiltered(filtered); // これが同期処理のため遅延の原因に
  };

  return (
    <>
      <input value={input} onChange={onChange} />
      <ul>
        {filtered.map(item => <li key={item}>{item}</li>)}
      </ul>
    </>
  );
}

上記では、毎回フィルター処理が即時実行されるため、数千件以上のデータがあると入力が明らかに遅くなります。

after: useTransition導入(即時入力 × 遅延処理)

tsximport { useTransition, useState } from 'react';

function FilterList({ allItems }) {
  const [input, setInput] = useState('');
  const [filtered, setFiltered] = useState(allItems);
  const [isPending, startTransition] = useTransition();

  const onChange = (e) => {
    const value = e.target.value;
    setInput(value);

    startTransition(() => {
      const filtered = allItems.filter(item =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFiltered(filtered);
    });
  };

  return (
    <>
      <input value={input} onChange={onChange} />
      {isPending && <p>フィルター中...</p>}
      <ul>
        {filtered.map(item => <li key={item}>{item}</li>)}
      </ul>
    </>
  );
}

この例では、**UIの即時応答(setInput)重い処理の遅延実行(startTransition)**を明確に分離しています。

これにより、操作感はサクサクなのに、バックグラウンドではきちんと処理が進んでいるという理想的な体験が成立します。

SuspenseとuseTransitionの組み合わせによる実用パターン

React 18以降では、非同期UIの表現がより直感的に記述できるようになりました。
特に、Suspenseで非同期コンポーネントを包み、useTransitionでその切り替えを低優先度で行うパターンは実用的です。

ユーザー切り替えボタンの例

tsximport { Suspense, useTransition, useState } from 'react';

const UserDetail = React.lazy(() => import('./UserDetail'));

export default function App() {
  const [userId, setUserId] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleClick = (id: number) => {
    startTransition(() => {
      setUserId(id);
    });
  };

  return (
    <div>
      <button onClick={() => handleClick(1)}>ユーザー1</button>
      <button onClick={() => handleClick(2)}>ユーザー2</button>

      {isPending && <p>読み込み中...</p>}

      <Suspense fallback={<p>ユーザー情報を取得中...</p>}>
        {userId && <UserDetail userId={userId} />}
      </Suspense>
    </div>
  );
}

この構成では、ボタンを押した瞬間に isPending によってローディング表示が出て、裏でデータが読み込まれる仕組みになります。
ロジックと演出が分離されているため、UXが安定します。

フォールバックの工夫でさらに心地よく

Suspense に渡す fallback の中身は、ユーザー体験を左右する重要な部分です。
単純な「読み込み中...」よりも、以下のような演出的コンポーネントを使うと違和感が軽減されます。

よく使われる表現

表現パターン説明
スケルトンUI実際のレイアウトを模したグレーボックス表示
shimmer エフェクトローディング感のあるアニメーション表示
プレースホルダー「名前を取得中…」などの文言
tsxfunction SkeletonUserDetail() {
  return (
    <div className=\"skeleton\">
      <div className=\"avatar shimmer\" />
      <div className=\"line shimmer\" />
      <div className=\"line shimmer short\" />
    </div>
  );
}

CSSで shimmer アニメーションを加えることで、ユーザーに「動いている感」を伝えることができます。

サーバーコンポーネントとの親和性

Next.js 13以降では app/ ディレクトリを利用することで、React Server Components(RSC)と Suspense をより自然に組み合わせることができます。

tsx// app/page.tsx
import { Suspense } from 'react';
import UserList from './UserList';

export default function Page() {
  return (
    <Suspense fallback={<p>ユーザー一覧を取得中...</p>}>
      <UserList />
    </Suspense>
  );
}

UserList コンポーネントが async 関数になっている場合、サーバーサイドで fetch() が行われ、読み込み完了まで fallback が表示されます。

公式ガイド - Suspense for Data Fetching

本番運用でのベストプラクティス

実際にプロダクション環境で活用する際の注意点と工夫を以下にまとめます。

1. Suspenseをネストして粒度を分ける

複数の非同期データがある場合、一括読み込みではなく個別にSuspenseで包むことで、ローディング範囲を限定できます。

tsx<Suspense fallback={<SkeletonUser />}>
  <User />
</Suspense>

<Suspense fallback={<SkeletonPost />}>
  <Post />
</Suspense>

2. useTransitionで遅延表示を自然に

isPendingを使ってボタンやフィルターにアニメーション付きの状態表示を組み込むと、ユーザーの理解を助けます。

tsx<button disabled={isPending}>
  {isPending ? '検索中...' : '検索'}
</button>

3. 低スペック端末向けの最適化

useTransitionは、CPU負荷の高い環境でもUIの“引っかかり”を軽減する効果があります。
特にモバイルや仮想環境でのUX改善に有効です。

まとめ(再掲)

React 18以降のアプリ開発では、単に動くことではなく、自然に動くことが重要なテーマとなっています。

その中で SuspenseuseTransition は、非同期処理によるストレスを限りなく減らし、直感的で快適な操作体験を実現するための中核技術です。

ぜひこれらを適切に活用し、ユーザーに“気持ちよく使える”Webアプリケーションを届けてください。

詳しくは公式ドキュメントをご参照ください。

記事Article

もっと見る