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

ReactアプリのUXを一段階進化させるなら、この組み合わせが鍵となります。
Suspense
と useTransition
を活用することで、重たいデータ読み込みや画面遷移時にも、ユーザーにストレスを与えない滑らかな操作体験を提供できます。
この記事では、それぞれの役割を丁寧に解説し、実際にアプリに導入する手順と注意点を、豊富なサンプルコードとともにお届けいたします。
なぜ滑らかな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
が表示されます。
本番運用でのベストプラクティス
実際にプロダクション環境で活用する際の注意点と工夫を以下にまとめます。
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以降のアプリ開発では、単に動くことではなく、自然に動くことが重要なテーマとなっています。
その中で Suspense
と useTransition
は、非同期処理によるストレスを限りなく減らし、直感的で快適な操作体験を実現するための中核技術です。
ぜひこれらを適切に活用し、ユーザーに“気持ちよく使える”Webアプリケーションを届けてください。
詳しくは公式ドキュメントをご参照ください。
記事Article
もっと見る- article
React Suspenseを使う際に避けたいアンチパターン5選と解決策について紹介
- article
React SuspenseとServer Componentsの融合:クライアントとサーバの役割分担
- article
React Suspenseでデータフェッチ!fetchでは動かない理由と正しい書き方について紹介
- article
React 18のSuspense完全対応ガイド:並列レンダリング時代の新常識
- article
Suspense × lazyで始めるコード分割:Reactアプリの初歩的最適化
- article
React Suspense入門:非同期UIを直感的に書ける新しいアプローチとは?