T-CREATOR

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

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でデータ取得するために、キャッシュ付きのラッパーを自作する方法を紹介しています。

公式解説:React - Rendering Strategies

キャッシュ付きリソースの構築

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開発をリードできるスキルを身につけましょう。

記事Article

もっと見る