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()には特別な能力がある:
- 条件付きで呼び出し可能(if文、ループ内でもOK)
- Suspenseと統合されてローディング状態を処理
- Error Boundaryと統合されてエラー処理
- Promiseから値を取り出せる(非同期の結果をそのまま使える)
基本的な使い方:
import { use } from 'react'; // Promiseと使う場合 const value = use(promise); // Contextと使う場合 const theme = use(ThemeContext);
中で何が起きてる?
use()をちゃんと使うには、Suspenseの仕組みを知っておく必要がある。
use(promise)を呼び出すと:
-
Promiseがpending状態なら:Reactがコンポーネントを「サスペンド」。Suspenseがキャッチする特別なオブジェクトをthrowしてfallback UIが表示される。
-
Promiseがresolveしたら:Reactがresolvedした値を即座に返す。
-
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()はさらに強力になる:
- Server Componentsで直接await可能
- 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()が常に正解ではない:
- ミューテーション:フォーム送信には
useTransitionやuseActionStateを使用 - リアルタイムデータ:サブスクリプション(WebSocketなど)には
useSyncExternalStore - ブラウザAPI:localStorage、window sizeなどは適切なフックを使用
- シンプルな状態:UI状態には普通に
useState
まとめ
use()フックはReact開発のパラダイムシフトを表している。「データをfetchしてsetStateする」命令型パターンから「このコンポーネントはこのデータが必要」という宣言型パターンへの移行。
ポイントまとめ:
use()でPromise/Contextから値を読む — Suspense連携あり- Promiseはキャッシュ必須 — しないと無限ループ
- Suspense = ローディング、Error Boundary = エラー
- if文の中でも呼べる — 他のフックと違う
- ルーター/ローダーで先にfetch — パフォーマンス最適化
- Server Componentsと相性◎ — ストリーミングできる
学習曲線はある。データフローについて違う考え方が必要。でも一度コツを掴めば、useEffectデータフェッチに戻りたくなくなる。
Reactチームが何年もこの瞬間のために作業してきた。React 19でビジョンがついに実現:コンポーネントが必要なデータを宣言するだけで、Reactがローディング、エラー状態、レースコンディションの全ての複雑さを処理する。
Reactデータフェッチの未来へようこそ。
// 未来はここに function UserProfile({ userId }) { const user = use(fetchUserCached(userId)); return <div>{user.name}</div>; }
本当にこれだけでいい。