Back

React 19 use()フック完全解説: データフェッチのパラダイムが変わる

React 19で多くの機能が追加されたけど、Reactコードの書き方を根本から変えるのはuse()フック。

Reactをしばらく書いてきたなら、このパターンを何百回も書いたことがあるはず:

function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function fetchUser() { try { setLoading(true); const response = await fetch(`/api/users/${userId}`); const data = await response.json(); if (!cancelled) { setUser(data); } } catch (err) { if (!cancelled) { setError(err); } } finally { if (!cancelled) { setLoading(false); } } } fetchUser(); return () => { cancelled = true; }; }, [userId]); if (loading) return <Spinner />; if (error) return <Error message={error.message} />; return <div>{user.name}</div>; }

概念的にはシンプル:「ユーザーデータを取得して名前を表示する」。でも40行

React 19のuse()フックで同じことをすると:

function UserProfile({ userId }) { const user = use(fetchUser(userId)); return <div>{user.name}</div>; }

3行。同じ機能。ローディング状態管理なし。エラー処理のボイラープレートなし。レースコンディションバグもなし。

シンタックスシュガーじゃない。Reactの非同期の扱い方が根本から変わった。どう動くか、いつ使うべきか、ハマりポイントは何かを見ていく。

use()フックとは?

use()フックはReact 19でPromiseやContextなどのリソースから値を読み取る方法。他のフックと違ってuse()には特別な能力がある:

  1. 条件付きで呼び出し可能(if文、ループ内でもOK)
  2. Suspenseと統合されてローディング状態を処理
  3. Error Boundaryと統合されてエラー処理
  4. Promiseから値を取り出せる(非同期の結果をそのまま使える)

基本的な使い方:

import { use } from 'react'; // Promiseと使う場合 const value = use(promise); // Contextと使う場合 const theme = use(ThemeContext);

中で何が起きてる?

use()をちゃんと使うには、Suspenseの仕組みを知っておく必要がある。

use(promise)を呼び出すと:

  1. Promiseがpending状態なら:Reactがコンポーネントを「サスペンド」。Suspenseがキャッチする特別なオブジェクトをthrowしてfallback UIが表示される。

  2. Promiseがresolveしたら:Reactがresolvedした値を即座に返す。

  3. Promiseがrejectしたら:Reactがエラーをthrowし、Error Boundaryがキャッチ。

シンプルなメンタルモデル:

function use(promise) { if (promise.status === 'pending') { throw promise; // Suspenseがこれをキャッチ } if (promise.status === 'rejected') { throw promise.reason; // Error Boundaryがこれをキャッチ } return promise.value; // resolvedした値を返す }

ポイント:use()は状態を持たない。「このデータちょうだい」ってReactに頼むだけ。

重要パターン:Promiseのキャッシュ

ここで多くの開発者が混乱する。これは動作しない

// ❌ 間違い:レンダーのたびに新しいPromise作成 function UserProfile({ userId }) { const user = use(fetch(`/api/users/${userId}`).then(r => r.json())); return <div>{user.name}</div>; }

なぜ?コンポーネントがレンダーされるたびに新しいPromiseが作られる。Reactは新しいPromiseを見てサスペンドし、fallbackを表示し、Promiseがresolveして、Reactが再レンダーして、新しいPromiseを作成…無限ループ。

Promiseはコンポーネントの外でキャッシュするか、安定した参照で保持する必要がある。

パターン1:親コンポーネントでキャッシュ

// ✅ 親でPromise作成、子に渡す function App() { const [userId, setUserId] = useState(1); const userPromise = useMemo( () => fetchUser(userId), [userId] ); return ( <Suspense fallback={<Spinner />}> <UserProfile userPromise={userPromise} /> </Suspense> ); } function UserProfile({ userPromise }) { const user = use(userPromise); return <div>{user.name}</div>; }

パターン2:データライブラリを使用

ほとんどのデータフェッチライブラリはすでにキャッシュを処理している:

// ✅ React Query / TanStack Query function UserProfile({ userId }) { const { data: user } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); return <div>{user.name}</div>; } // ✅ SWR with Suspense function UserProfile({ userId }) { const { data: user } = useSWR( `/api/users/${userId}`, fetcher, { suspense: true } ); return <div>{user.name}</div>; }

パターン3:シンプルなキャッシュを作成

より単純なケースでは自作のキャッシュを作れる:

// シンプルなキャッシュ実装 const cache = new Map(); function fetchUserCached(userId) { if (!cache.has(userId)) { cache.set(userId, fetchUser(userId)); } return cache.get(userId); } function UserProfile({ userId }) { const user = use(fetchUserCached(userId)); return <div>{user.name}</div>; }

use()とContext:useContextより強力

use()はContextも読み取れるけど、特別な能力がある:条件付きで呼び出し可能

// ❌ useContextは条件付き不可 function Button({ showTheme }) { // Rules of Hooks違反 if (showTheme) { const theme = useContext(ThemeContext); } } // ✅ use()は条件付き可能 function Button({ showTheme }) { if (showTheme) { const theme = use(ThemeContext); return <button style={{ color: theme.primary }}>Click</button>; } return <button>Click</button>; }

以前は不可能だったパターンが可能に:

function ConditionalFeature({ featureFlags }) { // 機能が認証を要求する場合のみauth contextにアクセス if (featureFlags.requiresAuth) { const auth = use(AuthContext); if (!auth.user) { return <LoginPrompt />; } } return <Feature />; }

use()のエラー処理

use()に渡されたPromiseがrejectすると、エラーをthrowする。Error Boundaryでキャッチする必要がある:

function App() { return ( <ErrorBoundary fallback={<ErrorMessage />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

より細かいエラー処理のためにError Boundaryをネストできる:

function Dashboard() { return ( <div className="dashboard"> <ErrorBoundary fallback={<UserError />}> <Suspense fallback={<UserSkeleton />}> <UserWidget /> </Suspense> </ErrorBoundary> <ErrorBoundary fallback={<StatsError />}> <Suspense fallback={<StatsSkeleton />}> <StatsWidget /> </Suspense> </ErrorBoundary> </div> ); }

再利用可能なError Boundaryの作成

プロダクションで使えるError Boundaryコンポーネント:

import { Component } from 'react'; class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error }; } componentDidCatch(error, errorInfo) { // エラー報告サービスにログ console.error('Error caught by boundary:', error, errorInfo); } render() { if (this.state.error) { return this.props.fallback ?? ( <div className="error-container"> <h2>問題が発生しました</h2> <button onClick={() => this.setState({ error: null })}> もう一度試す </button> </div> ); } return this.props.children; } }

並列データフェッチ

use()の最も強力なパターンの一つが並列データフェッチ。ウォーターフォールリクエストの代わりに一度に全部取得できる:

// ❌ ウォーターフォール:各fetchが前のを待つ function Dashboard({ userId }) { const user = use(fetchUser(userId)); const posts = use(fetchPosts(userId)); // userを待つ const comments = use(fetchComments(userId)); // postsを待つ return <DashboardView user={user} posts={posts} comments={comments} />; } // ✅ 並列:全fetchが同時に開始 function Dashboard({ userId }) { const userPromise = fetchUserCached(userId); const postsPromise = fetchPostsCached(userId); const commentsPromise = fetchCommentsCached(userId); const user = use(userPromise); const posts = use(postsPromise); const comments = use(commentsPromise); return <DashboardView user={user} posts={posts} comments={comments} />; }

キーポイント:並列版ではuse()呼び出し前に全Promiseが作られる。つまり全リクエストが即座に開始される。

さらに良い方法:ルートローダーでフェッチ

最適なパフォーマンスのために、できるだけ早くフェッチを開始する。理想的にはルーターで:

// React Routerローダー export async function dashboardLoader({ params }) { return { userPromise: fetchUser(params.userId), postsPromise: fetchPosts(params.userId), commentsPromise: fetchComments(params.userId), }; } function Dashboard() { const { userPromise, postsPromise, commentsPromise } = useLoaderData(); const user = use(userPromise); const posts = use(postsPromise); const comments = use(commentsPromise); return <DashboardView user={user} posts={posts} comments={comments} />; }

コンポーネントがレンダーされる前にフェッチが開始される!

use() vs 従来のパターン比較

use()を他のデータフェッチ方法と比較:

useEffect + useState方式

// 従来の方式 function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; setLoading(true); fetchUser(userId) .then(data => !cancelled && setUser(data)) .catch(err => !cancelled && setError(err)) .finally(() => !cancelled && setLoading(false)); return () => { cancelled = true; }; }, [userId]); if (loading) return <Spinner />; if (error) return <Error />; return <div>{user.name}</div>; }

問題点:

  • 冗長なボイラープレート
  • クリーンアップを忘れやすい
  • レースコンディション処理が手動
  • 全コンポーネントでloading/error状態管理

use()方式

// モダンな方式 function UserProfile({ userId }) { const userPromise = useMemo(() => fetchUser(userId), [userId]); const user = use(userPromise); return <div>{user.name}</div>; } // 上位レベルでSuspense/ErrorBoundaryでラップ function App() { return ( <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

メリット:

  • クリーンで読みやすいコンポーネントコード
  • Loading/error処理がビジネスロジックと分離
  • レースコンディションなし(Reactが処理)
  • 組み合わせ可能(Suspense境界をネストして細かいローディング状態)

よくある間違いと対処法

間違い1:コンポーネント内でPromise作成

// ❌ レンダーのたびに新しいPromise作成 function Bad({ id }) { const data = use(fetch(`/api/${id}`).then(r => r.json())); } // ✅ Promiseをキャッシュ const cache = new Map(); function Good({ id }) { if (!cache.has(id)) { cache.set(id, fetch(`/api/${id}`).then(r => r.json())); } const data = use(cache.get(id)); }

間違い2:Suspense Boundaryを忘れる

// ❌ Suspenseなし = Promiseがpendingの時クラッシュ function App() { return <UserProfile userId={1} />; } // ✅ Suspenseでラップ function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> ); }

間違い3:Error Boundaryを忘れる

// ❌ ErrorBoundaryなし = エラーがアプリをクラッシュ function App() { return ( <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> ); } // ✅ ErrorBoundary追加 function App() { return ( <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Spinner />}> <UserProfile userId={1} /> </Suspense> </ErrorBoundary> ); }

間違い4:Promise rejectionを適切に処理しない

// ❌ Fetchが静かに失敗する可能性 const promise = fetch('/api/data').then(r => r.json()); // ✅ HTTPエラーを処理 const promise = fetch('/api/data').then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); });

Server Componentsでのuse()

React Server Componentsでuse()はさらに強力になる:

  1. Server Componentsで直接await可能
  2. Client ComponentsにPromiseを渡すことが可能
// Server Component async function Page({ params }) { const userPromise = fetchUser(params.id); return ( <Suspense fallback={<Spinner />}> <UserProfile userPromise={userPromise} /> </Suspense> ); } // Client Component 'use client'; function UserProfile({ userPromise }) { const user = use(userPromise); return <div>{user.name}</div>; }

このパターンでストリーミングが可能に:サーバーがシェルを即座に送信し、データはバックグラウンドでロード。

パフォーマンス最適化のコツ

1. 早めにフェッチ開始

// ルーターやレイアウトで const userPromise = fetchUser(userId); // コンポーネントに渡す <UserProfile userPromise={userPromise} />

2. Suspenseでストリーミング活用

function Page() { return ( <> {/* 重要なコンテンツを先にロード */} <Header /> {/* 重要度の低いコンテンツはストリーミング */} <Suspense fallback={<CommentsSkeleton />}> <Comments /> </Suspense> </> ); }

3. 細かいSuspense境界

function Dashboard() { return ( <div className="grid"> <Suspense fallback={<CardSkeleton />}> <RevenueCard /> </Suspense> <Suspense fallback={<CardSkeleton />}> <UsersCard /> </Suspense> <Suspense fallback={<CardSkeleton />}> <ActivityCard /> </Suspense> </div> ); }

各カードが独立してロード。最も遅いものを待つ必要なし。

use()を使うべきでない場合

use()が常に正解ではない:

  1. ミューテーション:フォーム送信にはuseTransitionuseActionStateを使用
  2. リアルタイムデータ:サブスクリプション(WebSocketなど)にはuseSyncExternalStore
  3. ブラウザAPI:localStorage、window sizeなどは適切なフックを使用
  4. シンプルな状態:UI状態には普通にuseState

まとめ

use()フックはReact開発のパラダイムシフトを表している。「データをfetchしてsetStateする」命令型パターンから「このコンポーネントはこのデータが必要」という宣言型パターンへの移行。

ポイントまとめ:

  1. use()でPromise/Contextから値を読む — Suspense連携あり
  2. Promiseはキャッシュ必須 — しないと無限ループ
  3. Suspense = ローディングError Boundary = エラー
  4. if文の中でも呼べる — 他のフックと違う
  5. ルーター/ローダーで先にfetch — パフォーマンス最適化
  6. Server Componentsと相性◎ — ストリーミングできる

学習曲線はある。データフローについて違う考え方が必要。でも一度コツを掴めば、useEffectデータフェッチに戻りたくなくなる。

Reactチームが何年もこの瞬間のために作業してきた。React 19でビジョンがついに実現:コンポーネントが必要なデータを宣言するだけで、Reactがローディング、エラー状態、レースコンディションの全ての複雑さを処理する。

Reactデータフェッチの未来へようこそ。

// 未来はここに function UserProfile({ userId }) { const user = use(fetchUserCached(userId)); return <div>{user.name}</div>; }

本当にこれだけでいい。

reactreact-19use-hookdata-fetchingsuspensejavascriptfrontendasync