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.json の compilerOptions.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 を渡すパターンを活用すると、データのウォーターフォール問題を解決しながらストリーミングレンダリングの恩恵を受けられます。
