React Suspenseでデータフェッチ!fetchでは動かない理由と正しい書き方について紹介

Reactで非同期処理を扱う際、「fetch
でデータを取得してuseEffect
で状態管理」という流れに慣れ親しんでいる方は多いはずです。
ですが、React 18のSuspenseでデータフェッチを行おうとすると、「fetch
がそのままでは動かない」という壁に直面することがあります。
本記事では、なぜ従来のfetch
ではSuspenseと組み合わせられないのか、その根本的な理由と、React 18以降での正しいデータフェッチの方法について、サンプルコードを交えて丁寧にご紹介いたします。
fetchではSuspenseに対応できない理由
結論から申しますと、fetch
関数は一度呼び出して完了を待つ仕組みであり、Reactの描画ライフサイクルとは連携していません。
Suspenseは、Promiseがthrowされた時点で描画を一時停止し、fallback UIを表示する仕組みです。しかし、通常のfetch
は以下のように状態に閉じ込めて使うため、Reactのレンダリング中にPromiseをthrowすることができません。
tsx// NG例
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then((json) => setData(json));
}, []);
このコードでは、fetch()
の呼び出しはレンダリングとは独立したタイミングで実行され、Suspenseが関知できないのです。
Suspenseが扱えるのは、"描画中に投げられるPromise" のみです。
正しいパターン:React 18のuse()
とServer Componentを使う
React 18からは、Server Component + use()
関数という新しいAPIが導入されました。
これを活用することで、Suspenseと連携できる形でデータフェッチが可能になります。
公式リンク:React - use
基本構文
tsx// app/page.tsx (Server Component)
import { use } from 'react';
async function getData() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('データ取得失敗');
return res.json();
}
export default function Page() {
const posts = use(getData());
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
ポイント:
項目 | 解説 |
---|---|
getData() | async関数としてPromiseを返す |
use() | Promiseを描画中にthrowし、Suspenseでキャッチできるようにする |
Server Component | クライアントではなく、サーバー上で実行される |
Fallbackを表示する
tsximport { Suspense } from 'react';
import PostList from './PostList';
export default function Page() {
return (
<Suspense fallback={<div>読み込み中です...</div>}>
<PostList />
</Suspense>
);
}
クライアント側ではuse()
は使えない
use()
はサーバーコンポーネント専用のAPIです。
クライアントコンポーネントで同じことをやろうとするとエラーになります。
ではクライアント側でSuspenseを使ったデータフェッチをしたい場合、どうすればよいのでしょうか?
クライアントでも使える:リソースキャッシュの自作
React公式は、クライアントサイドにおいてSuspenseでデータ取得するために、キャッシュ付きのラッパーを自作する方法を紹介しています。
キャッシュ付きリソースの構築
tsx// lib/createResource.ts
export function createResource(fetcher: () => Promise<any>) {
let status = 'pending';
let result: any;
let suspender = fetcher().then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') throw suspender;
if (status === 'error') throw result;
return result;
},
};
}
コンポーネントでの使用方法
tsx// lib/resource.ts
import { createResource } from './createResource';
export const postResource = createResource(() =>
fetch('https://jsonplaceholder.typicode.com/posts').then((res) => res.json())
);
tsx// components/PostList.tsx
import { postResource } from '../lib/resource';
export default function PostList() {
const posts = postResource.read();
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
tsx// app/page.tsx
import { Suspense } from 'react';
import PostList from './components/PostList';
export default function Page() {
return (
<Suspense fallback={<div>読み込み中...</div>}>
<PostList />
</Suspense>
);
}
このようにして、クライアント側でPromiseをthrowし、Suspenseでfallbackを表示する仕組みを構築することができます。
React QueryやSWRとの比較と併用
React QueryやSWRのような状態管理ライブラリとSuspenseを組み合わせることも可能です。
たとえばReact Queryでは、suspense: true
をオプションで指定することで、<Suspense>
に対応可能になります。
tsxconst { data } = useQuery(['posts'], fetchPosts, {
suspense: true,
});
ただし、これらのライブラリは再利用性やキャッシュ戦略が高度に設計されているため、基本的には非同期の状態管理に集中し、UIはSuspenseで最小限に巻き取る使い方がベストです。
React Query公式:https://tanstack.com/query/latest/docs/react/guides/suspense
よくあるミスとデバッグのヒント
ミス | 原因 | 解決策 |
---|---|---|
use() が動かない | クライアント側で使用している | Server Componentで使用する |
fetch だけでSuspenseが効かない | Promiseをthrowしていない | キャッシュ付きラッパーを作る |
Suspense内のエラーが表示されない | ErrorBoundaryがない | ErrorBoundaryで囲む |
実践Tipsまとめ
シーン | 手法 |
---|---|
サーバー側で静的フェッチ | use(fetcher()) |
クライアントで状態持たずに表示だけ | createResource() パターン |
高度なキャッシュ戦略が必要 | React Query / SWR + Suspense |
SSRで段階的描画 | Streaming SSR + Suspense |
まとめ
React 18でのSuspenseによるデータフェッチは、描画ライフサイクルの中でPromiseをthrowするという新しい概念に基づいています。
そのため、単なるfetch
の呼び出しでは不十分であり、use()
フックや、Promiseをthrowする仕組みを自作することが必要となります。
Suspenseによって非同期UIの表現力は大きく向上しましたが、それを正しく活かすには「従来の非同期処理との違い」を深く理解しておく必要があります。
今後、Reactの標準データ取得戦略はこのような形にシフトしていくことが予想されます。ぜひ早い段階で仕組みに慣れて、モダンなReact開発をリードできるスキルを身につけましょう。