← 記事一覧に戻る

React 19 の use() でデータ取得が動かない・Suspense エラーになる時の5つの原因と対策【Promise / Context / Next.js App Router】

React 19 の use() でデータ取得が動かない

React 19 で新たに導入された use() フックは、Promise や Context を読み取るための新しい方法です。従来のフックと異なり、条件分岐の中でも使えるという特性がありますが、同時にいくつかの落とし穴があります。

// 基本的な使い方
import { use, Suspense } from 'react'

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise)
  return <div>{user.name}</div>
}

export default function Page() {
  return (
    <Suspense fallback={<div>読み込み中...</div>}>
      <UserProfile userPromise={fetchUser()} />
    </Suspense>
  )
}

一見シンプルですが、このコードには致命的な問題があります。

原因1:毎回新しい Promise を生成して無限ループになる

上記の fetchUser() を JSX の中で直接呼び出すと、レンダリングのたびに新しい Promise が生成され、無限ループになります。

// NG: レンダリングのたびに新しい Promise が生成される
export default function Page() {
  return (
    <Suspense fallback={<div>読み込み中...</div>}>
      <UserProfile userPromise={fetchUser()} /> {/* 毎回新しい Promise */}
    </Suspense>
  )
}

React は use() が Promise を受け取ると、その Promise が pending の間コンポーネントをサスペンドします。サスペンドから復帰後に再レンダリングされると、また新しい Promise が渡され…という無限ループになります。

対処法:Promise を上位スコープで作成するか useState を使う

// OK パターン1: Promise をモジュールスコープで1回だけ作成
const userPromise = fetchUser()

export default function Page() {
  return (
    <Suspense fallback={<div>読み込み中...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

または useState の初期値として渡すと1回だけ実行されます:

import { use, Suspense, useState } from 'react'

export default function Page() {
  // useState の初期化関数として渡すと初回レンダリング時のみ実行
  const [userPromise] = useState(() => fetchUser())

  return (
    <Suspense fallback={<div>読み込み中...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

原因2:Suspense 境界がなくてエラーになる

use() に Promise を渡すと、コンポーネントは必ずサスペンドします。サスペンドするコンポーネントは Suspense 境界で囲わないと、アプリ全体がエラーになります

// NG: Suspense がない
function App() {
  return <UserProfile userPromise={fetchUser()} />
  // → A component suspended while responding to synchronous input.
}

// OK: Suspense で囲む
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userPromise={fetchUser()} />
    </Suspense>
  )
}

エラーハンドリングには ErrorBoundary が必要

Promise が reject した場合、use() はエラーをスローします。これを捕捉するには ErrorBoundary が必要です:

import { ErrorBoundary } from 'react-error-boundary'

function App() {
  const [userPromise] = useState(() => fetchUser())

  return (
    <ErrorBoundary fallback={<p>エラーが発生しました</p>}>
      <Suspense fallback={<Spinner />}>
        <UserProfile userPromise={userPromise} />
      </Suspense>
    </ErrorBoundary>
  )
}

react-error-boundary ライブラリを使うと、クラスコンポーネントなしでエラーバウンダリを実装できます:

npm install react-error-boundary

原因3:Next.js Server Components では use() を使わない

Next.js App Router の Server Components では use() は不要です。async/await を直接使えます。

// NG: Server Component で use() を使う(動くが冗長・本来の用途ではない)
async function ServerPage() {
  const dataPromise = fetchData()
  const data = use(dataPromise)
  return <div>{data.title}</div>
}

// OK: Server Component では async/await を使う
async function ServerPage() {
  const data = await fetchData()
  return <div>{data.title}</div>
}

use() が真価を発揮するのは Client Components でデータを受け取る場合です。Server Component でデータフェッチを開始し、その Promise を Client Component に渡すパターンが推奨されています:

// page.tsx(Server Component)
export default function Page() {
  // await せずに Promise をそのまま渡す
  const userPromise = fetchUser()

  return (
    <Suspense fallback={<Skeleton />}>
      <UserCard userPromise={userPromise} />
    </Suspense>
  )
}
// UserCard.tsx(Client Component)
'use client'
import { use } from 'react'

type Props = { userPromise: Promise<User> }

export function UserCard({ userPromise }: Props) {
  const user = use(userPromise)
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

このパターンにより、Server → Client へのデータストリーミングが実現でき、ページ全体の読み込みをブロックせずに UI を段階的に表示できます。

原因4:use() で Context を読み取る時の null クラッシュ

use() は Context の読み取りにも使えます。条件分岐の中で呼べるのが useContext との最大の違いです。

import { use, createContext } from 'react'

const ThemeContext = createContext<'light' | 'dark'>('light')

function Button({ showTheme }: { showTheme: boolean }) {
  // useContext と違い、条件分岐の中でも呼べる
  if (showTheme) {
    const theme = use(ThemeContext)
    return <button className={theme}>クリック</button>
  }
  return <button>クリック</button>
}

ただし、Context の初期値が null の場合にクラッシュすることがあります:

// NG: Context が null になりうる場合
const UserContext = createContext<User | null>(null)

function Profile() {
  const user = use(UserContext)
  return <div>{user.name}</div> // TypeError: Cannot read properties of null
}

// OK: null チェックを行う
function Profile() {
  const user = use(UserContext)
  if (!user) return <div>ログインしてください</div>
  return <div>{user.name}</div>
}

型安全にするには、Provider がない場合にエラーをスローするカスタムフックにラップするのが実務では有効です:

function useUser(): User {
  const user = use(UserContext)
  if (!user) throw new Error('UserContext が Provider で囲まれていません')
  return user
}

原因5:TypeScript の型定義が古くて use() が認識されない

React 19 の use() は比較的新しい API のため、古い @types/react では型定義が存在しない場合があります。

TypeScript error: Module '"react"' has no exported member 'use'.

対処法:@types/react を最新バージョンにアップデートします。

npm install @types/react@latest @types/react-dom@latest
# あわせて react 本体も更新
npm install react@latest react-dom@latest

また、tsconfig.jsoncompilerOptions.lib"ES2015" 以上が含まれていることを確認してください:

{
  "compilerOptions": {
    "lib": ["ES2020", "DOM"],
    "jsx": "react-jsx",
    "moduleResolution": "bundler"
  }
}

型チェックを通しても実行時エラーになる場合は、バンドラーが React 19 に対応しているか確認が必要です。Vite を使っている場合は vite@latest に更新することで解決することが多いです。

use() vs useEffect + useState パターンの使い分け

use() と従来の useEffect + useState パターンはどちらを使うべきでしょうか?

パターン 用途 メリット デメリット
use(Promise) 親から Promise を受け取る場合 Suspense と統合、ウォーターフォール回避 Promise 管理が必要
useEffect + useState コンポーネント内でフェッチする場合 独立したデータ取得、再フェッチが容易 ローディング状態を自前で管理
TanStack Query / SWR 複雑なデータ取得 キャッシュ・再フェッチ・エラー処理が充実 ライブラリの学習コスト

use() は「Promise を props として受け取って表示する」用途に特化しています。コンポーネント内でデータフェッチを開始する場合は TanStack Query や SWR のほうが実務向きです。

use() が特に有効なケース

Next.js App Router で Server Component からデータフェッチを開始し、Client Component で表示する場合が最も恩恵を受けやすいパターンです:

// app/dashboard/page.tsx(Server Component)
export default function DashboardPage() {
  // 複数のデータを並列でフェッチ開始(await しない)
  const userPromise = fetchUser()
  const postsPromise = fetchPosts()
  const statsPromise = fetchStats()

  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <UserCard userPromise={userPromise} />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <PostsList postsPromise={postsPromise} />
      </Suspense>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsPanel statsPromise={statsPromise} />
      </Suspense>
    </div>
  )
}

この構造により、それぞれのデータが独立して並列フェッチされ、準備できたコンポーネントから順に表示されます。

まとめ

症状 原因 対処法
無限ループ・パフォーマンス悪化 レンダリングのたびに Promise を再生成 useState(() => fetch()) やモジュールスコープで作成
アプリ全体がクラッシュする Suspense 境界がない <Suspense> で囲む
fetch エラーでクラッシュする ErrorBoundary がない react-error-boundary を追加
Server Component で冗長な記述になる async/await を使うべき場面で use() を使用 Server Components では async/await を直接使う
型エラー「‘use’ is not exported」 @types/react が古い @types/react@latest にアップデート

React 19 の use() は強力ですが、「Promise を再生成しない」「Suspense で囲む」 の2点を押さえるだけで、多くの問題は防げます。特に Next.js App Router と組み合わせる際は、Server Component → Client Component へ Promise を渡すパターンを活用すると、データのウォーターフォール問題を解決しながらストリーミングレンダリングの恩恵を受けられます。