最終更新:
React Suspense完全ガイド: データフェッチングと並行レンダリング
React Suspenseとは?
React Suspenseは、コンポーネントがレンダリングに必要なデータやリソースを待機している状態を宣言的に扱う仕組みです。React 16.6で実験的機能として導入され、React 18で並行レンダリングと統合され、React 19でさらに強化されました。
従来のデータフェッチングの課題
// ❌ 従来のパターン
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return null;
return <div>{user.name}</div>;
}
問題点:
- 各コンポーネントでローディング・エラー状態を管理
- ウォーターフォール問題(親→子の順次読み込み)
- レンダリングロジックとデータフェッチングが密結合
Suspenseを使った宣言的アプローチ
// ✅ Suspenseパターン
function UserProfile({ userId }: { userId: string }) {
const user = use(fetchUser(userId)); // React 19の use フック
return <div>{user.name}</div>;
}
function App() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Spinner />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
);
}
利点:
- コンポーネントはデータがあることを前提に書ける
- ローディング状態は親が管理
- エラーはErrorBoundaryで一元管理
- 並行レンダリングで複数のデータを並列取得
Suspenseの基本原理
Suspenseの動作メカニズム
SuspenseはPromiseを使って動作します。
function wrapPromise<T>(promise: Promise<T>) {
let status: 'pending' | 'success' | 'error' = 'pending';
let result: T;
let error: Error;
const suspender = promise.then(
(data) => {
status = 'success';
result = data;
},
(err) => {
status = 'error';
error = err;
}
);
return {
read(): T {
if (status === 'pending') {
throw suspender; // Promiseをthrowする!
}
if (status === 'error') {
throw error;
}
return result;
}
};
}
動作フロー:
- コンポーネントがPromiseをthrowする
- Reactが最も近い
<Suspense>境界を見つける - fallbackをレンダリング
- Promiseが解決されたら、コンポーネントを再レンダリング
React 19のuseフック
React 19で導入されたuseフックは、Promiseを直接扱える革新的な機能です。
import { use, Suspense } from 'react';
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserProfile({ userId }: { userId: string }) {
// use()でPromiseを直接読み取る
const user = use(fetchUser(userId));
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userId="123" />
</Suspense>
);
}
useの特徴
1. 条件付き呼び出しが可能
function Component({ userId }: { userId: string | null }) {
// useとは違い、条件分岐の中で使える!
const user = userId ? use(fetchUser(userId)) : null;
return user ? <div>{user.name}</div> : <div>No user</div>;
}
2. Contextも読み取れる
function Component() {
const theme = use(ThemeContext);
return <div className={theme}>Content</div>;
}
3. Server ComponentsとClient Componentsで共通
// Server Component
async function ServerUserProfile({ userId }: { userId: string }) {
const user = await fetchUser(userId); // awaitが使える
return <div>{user.name}</div>;
}
// Client Component
'use client';
function ClientUserProfile({ userId }: { userId: string }) {
const user = use(fetchUser(userId)); // useで同じことを実現
return <div>{user.name}</div>;
}
実践パターン
パターン1: 並列データフェッチング
interface Props {
userId: string;
}
// ❌ ウォーターフォール(遅い)
function UserDashboard({ userId }: Props) {
return (
<div>
<Suspense fallback={<Spinner />}>
<UserProfile userId={userId} />
{/* UserProfileが完了してから開始 */}
<UserPosts userId={userId} />
</Suspense>
</div>
);
}
// ✅ 並列フェッチング(速い)
function UserDashboard({ userId }: Props) {
// Promiseを先に作成
const userPromise = fetchUser(userId);
const postsPromise = fetchUserPosts(userId);
return (
<div>
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={userPromise} />
<UserPosts postsPromise={postsPromise} />
</Suspense>
</div>
);
}
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
function UserPosts({ postsPromise }: { postsPromise: Promise<Post[]> }) {
const posts = use(postsPromise);
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
パターン2: 段階的なSuspense境界
function UserDashboard({ userId }: Props) {
return (
<div>
{/* 重要なコンテンツは優先表示 */}
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
{/* 補助的なコンテンツは独立して読み込み */}
<Suspense fallback={<PostsSkeleton />}>
<UserPosts userId={userId} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<UserComments userId={userId} />
</Suspense>
</div>
);
}
利点:
- ユーザープロファイルが先に表示される
- 各セクションが独立して読み込まれる
- 一部のエラーが全体に影響しない
パターン3: useTransitionとの組み合わせ
import { useState, useTransition, Suspense } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const newQuery = e.target.value;
// 緊急度が低い更新としてマーク
startTransition(() => {
setQuery(newQuery);
});
};
return (
<div>
<input
type="text"
onChange={handleSearch}
placeholder="Search..."
style={{ opacity: isPending ? 0.5 : 1 }}
/>
<Suspense fallback={<SearchSkeleton />}>
<Results query={query} />
</Suspense>
</div>
);
}
function Results({ query }: { query: string }) {
const results = use(searchAPI(query));
return (
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
);
}
動作:
- ユーザーが入力すると
startTransitionが呼ばれる - 入力はすぐに反映(緊急更新)
- 検索結果の更新は低優先度(非緊急更新)
- 新しい結果が来るまで古い結果を表示し続ける
パターン4: useDeferredValueでデバウンス不要
import { useDeferredValue, Suspense } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{/* deferredQueryは遅延して更新される */}
<Suspense fallback={<Spinner />}>
<Results query={deferredQuery} />
</Suspense>
</div>
);
}
useDeferredValueの利点:
- デバウンスロジックが不要
- Reactが自動的に適切なタイミングで更新
- キャンセル処理も不要
TanStack Query(React Query)との統合
TanStack Query v5は、Suspenseをネイティブサポートしています。
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// dataは常にnon-null(Suspenseが保証)
return <div>{user.name}</div>;
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<Spinner />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
</QueryClientProvider>
);
}
prefetchQueryで高速化
import { useQueryClient } from '@tanstack/react-query';
function UserList() {
const queryClient = useQueryClient();
const { data: users } = useSuspenseQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
<ul>
{users.map(user => (
<li
key={user.id}
onMouseEnter={() => {
// ホバー時にプリフェッチ
queryClient.prefetchQuery({
queryKey: ['user', user.id],
queryFn: () => fetchUser(user.id),
});
}}
>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}
ErrorBoundaryとの連携
SuspenseはPromiseを、ErrorBoundaryはエラーをキャッチします。
import { Component, ReactNode } from 'react';
interface Props {
fallback: ReactNode;
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// 使用例
function App() {
return (
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<Loading />}>
<UserDashboard userId="123" />
</Suspense>
</ErrorBoundary>
);
}
react-error-boundaryライブラリの活用
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// リセット時の処理
window.location.href = '/';
}}
>
<Suspense fallback={<Loading />}>
<UserDashboard userId="123" />
</Suspense>
</ErrorBoundary>
);
}
Streaming SSR(Server-Side Rendering)
React 18以降、Suspenseはサーバーサイドでも動作します。
Next.js App Routerでの実装
// app/users/[id]/page.tsx
import { Suspense } from 'react';
async function UserProfile({ userId }: { userId: string }) {
// Server Componentでawait可能
const user = await fetchUser(userId);
return <div>{user.name}</div>;
}
async function UserPosts({ userId }: { userId: string }) {
const posts = await fetchUserPosts(userId);
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export default function UserPage({ params }: { params: { id: string } }) {
return (
<div>
{/* プロファイルは優先してストリーミング */}
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={params.id} />
</Suspense>
{/* 投稿は遅延ストリーミング */}
<Suspense fallback={<PostsSkeleton />}>
<UserPosts userId={params.id} />
</Suspense>
</div>
);
}
動作:
- HTMLの初期部分を即座に送信
- UserProfileが完了したらストリーミング
- UserPostsが完了したら追加でストリーミング
- JavaScriptでハイドレーション
SSRのパフォーマンス比較
従来のSSR:
- サーバーですべてのデータを取得(遅い)
- 完全なHTMLを生成してから送信
- TTFBが遅い
Streaming SSR with Suspense:
- HTMLをチャンク単位で送信(速い)
- 重要な部分を優先表示
- TTFBが早い
// 従来: すべて待つ
export async function getServerSideProps() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return { props: { user, posts, comments } };
}
// Streaming: 段階的に送信
export default function Page() {
return (
<>
<Suspense fallback={<UserSkeleton />}>
<User />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</>
);
}
画像・コンポーネントの遅延読み込み
React.lazyとSuspense
import { lazy, Suspense } from 'react';
// コンポーネントを動的インポート
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function Dashboard() {
const [showAdmin, setShowAdmin] = useState(false);
return (
<div>
<button onClick={() => setShowAdmin(!showAdmin)}>
Toggle Admin
</button>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
{showAdmin && (
<Suspense fallback={<div>Loading admin...</div>}>
<AdminPanel />
</Suspense>
)}
</div>
);
}
Next.js dynamic import
import dynamic from 'next/dynamic';
const DynamicMap = dynamic(() => import('./Map'), {
loading: () => <p>Loading map...</p>,
ssr: false, // クライアントでのみ読み込み
});
function Page() {
return (
<div>
<h1>Location</h1>
<DynamicMap />
</div>
);
}
パフォーマンス最適化
1. Suspense境界の粒度
// ❌ 粒度が粗い:1つでもエラーだと全体が失敗
<Suspense fallback={<BigSpinner />}>
<ComponentA />
<ComponentB />
<ComponentC />
</Suspense>
// ✅ 適切な粒度:独立して読み込み・エラーハンドリング
<>
<Suspense fallback={<SkeletonA />}>
<ComponentA />
</Suspense>
<Suspense fallback={<SkeletonB />}>
<ComponentB />
</Suspense>
<Suspense fallback={<SkeletonC />}>
<ComponentC />
</Suspense>
</>
2. キャッシュ戦略
// SWRのキャッシュ
import useSWR from 'swr';
function useUserSuspense(userId: string) {
const { data } = useSWR(
['user', userId],
() => fetchUser(userId),
{
suspense: true,
revalidateOnFocus: false,
dedupingInterval: 60000, // 1分間はキャッシュ
}
);
return data;
}
3. Prefetchingでウォーターフォール回避
function UserPage({ userId }: { userId: string }) {
// コンポーネント外でPromiseを作成
const userPromise = useMemo(() => fetchUser(userId), [userId]);
const postsPromise = useMemo(() => fetchPosts(userId), [userId]);
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<UserPosts postsPromise={postsPromise} />
</Suspense>
</div>
);
}
トラブルシューティング
Suspenseループ
// ❌ 無限ループ:毎回新しいPromiseを作成
function BadComponent() {
const data = use(fetchData()); // 毎レンダリングで新しいPromise
return <div>{data}</div>;
}
// ✅ 解決策:Promiseをメモ化
function GoodComponent() {
const dataPromise = useMemo(() => fetchData(), []);
const data = use(dataPromise);
return <div>{data}</div>;
}
// ✅ または外部で作成
const dataPromise = fetchData();
function GoodComponent() {
const data = use(dataPromise);
return <div>{data}</div>;
}
TypeScriptの型エラー
// use()の型推論
function Component() {
// 型が正しく推論される
const user = use(fetchUser('123')); // user: User
// 条件付きの場合
const maybeUser = userId ? use(fetchUser(userId)) : null;
// maybeUser: User | null
}
まとめ
React Suspenseは、データフェッチングとUIの関係を根本から変える革新的な機能です。
重要なポイント:
- 宣言的なコード - ローディング状態を親で管理
- 並行レンダリング - 複数のデータを並列取得
- 段階的な境界 - 重要度に応じて分割
- useTransition/useDeferredValue - 緊急度の制御
- Streaming SSR - サーバーサイドでの段階的レンダリング
- TanStack Query連携 - 実用的なデータ管理
React 19のuseフックとSuspenseを組み合わせることで、クリーンで保守性の高いデータフェッチングコードを実現できます。2026年のReact開発において、Suspenseは必須の知識です。