Contents
use() の概要と現状
React 19 では、非同期データ取得を宣言的に扱うことを目的に、use() という新しいフックが提案されました[^2]。このフックは Promise を直接受け取り、内部でサスペンド(中断)しながら結果が得られるまでコンポーネントのレンダリングを保留します。
| 特徴 | 説明 |
|---|---|
| 入力 | 任意の Promise<T>(React がサスペンド可能なもの) |
| 戻り値 | 解決された T (型情報はそのまま引き継がれる) |
| 連携先 | <Suspense> と組み合わせてローディング UI を自動表示 |
| 状態管理 | React が内部キャッシュを保持し、同一 Promise は再利用される |
| 安定性 | 現在は experimental(α)段階。将来の API 変更や削除の可能性あり |
公式ドキュメントでは「実験的機能として提供」されている旨が明記されています[^1]。そのため、以下のガイドラインは 開発・プロトタイプ 向けであることをご留意ください。
シンプルな非同期取得例
|
1 2 3 4 5 6 7 |
// src/api.ts export async function fetchUser(id: string) { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`); if (!res.ok) throw new Error('Network error'); return res.json(); // → { id, name, email, … } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/components/UserProfile.tsx import { use } from "react"; import { fetchUser } from "../api"; export function UserProfile({ userId }: { userId: string }) { const data = use(fetchUser(userId)); // ← Promise を直接渡す return ( <section> <h2>{data.name}</h2> <p>{data.email}</p> </section> ); } |
上記コンポーネントは fetchUser が解決するまで サスペンド 状態となり、親に配置した <Suspense> の fallback が自動的に表示されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/App.tsx import { Suspense } from "react"; import { UserProfile } from "./components/UserProfile"; export default function App() { return ( <Suspense fallback={<div>Loading user…</div>}> <UserProfile userId="1" /> </Suspense> ); } |
Loading と Error のハンドリング
エラーバウンダリの基本形
use() がサスペンド中に例外をスローすると、React は自動的に Error Boundary にエラーを転送します。下記は最小構成です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/components/ErrorBoundary.tsx import { Component, ReactNode } from "react"; type Props = { fallback: ReactNode; children: ReactNode; }; export class ErrorBoundary extends Component<Props, { hasError: boolean }> { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) return this.props.fallback; return this.props.children; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/App.tsx(更新版) import { Suspense } from "react"; import { UserProfile } from "./components/UserProfile"; import { ErrorBoundary } from "./components/ErrorBoundary"; export default function App() { return ( <ErrorBoundary fallback={<div>データ取得に失敗しました。</div>}> <Suspense fallback={<div>Loading user…</div>}> <UserProfile userId="1" /> </Suspense> </ErrorBoundary> ); } |
リトライ UI の実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// src/components/RetryButton.tsx import { useState } from "react"; type Props = { onRetry: () => void; }; export function RetryButton({ onRetry }: Props) { const [pending, setPending] = useState(false); return ( <button disabled={pending} onClick={() => { setPending(true); onRetry(); // 再描画が走ったらボタンは自動的に有効になる(ErrorBoundary が再マウントされるため) }} > {pending ? "リトライ中…" : "もう一度試す"} </button> ); } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/App.tsx(エラーバウンダリ内部で使用) import { RetryButton } from "./components/RetryButton"; <ErrorBoundary fallback={ <div> データ取得に失敗しました。 <RetryButton onRetry={() => window.location.reload()} /> </div> } > {/* … */} </ErrorBoundary> |
実装上の注意点とベストプラクティス
- 実験的であることを明示
-
package.jsonのdependenciesに"react": "19.0.0-alpha"を記載し、README などに「α版」旨を書いておく。 -
キャッシュ戦略の検討
-
同一 Promise は React が内部で再利用しますが、HTTP キャッシュヘッダーや SWR / TanStack Query と併用すると、データの鮮度管理が容易になります。
use()のみで高度なキャッシュ制御はできません。 -
AbortController との組み合わせ
- 長時間実行されるフェッチは
AbortControllerを使用し、コンポーネントがアンマウントされたときに中止させる。use()自体はキャンセルを認識しないため、エラーバウンダリでAbortErrorを捕捉する必要があります。
tsx
const controller = new AbortController();
use(fetch(url, { signal: controller.signal }));
// コンポーネントがアンマウントされたら:
// return () => controller.abort();
- 粒度の小さな ErrorBoundary
-
大きな UI 全体を 1 つのエラーバウンダリで包むと、1 部分だけ失敗しても全体が白紙になる恐れがあります。データ取得単位ごとに小さめの境界を設けると UX が向上します。
-
代替手段としてのクエリライブラリ
- プロダクションで安定した非同期管理が必要な場合は、
react-query(現 TanStack Query)やSWRの方が成熟度・機能面で有利です。use()は 概念実証(Proof‑of‑Concept) としての位置付けを意識してください。
カスタムフックで永続化ロジックを抽象化する
React では「状態管理はフックに切り出す」というパターンが推奨されています。use() が 非同期取得 に特化しているのに対し、ローカルストレージへの永続化 は全く別の関心事です。そのため、以下では useLocalStorage と useFormPersist の 2 つのカスタムフックを例示します。これらは React 19 に依存しない 完全な汎用フックです。
useLocalStorage の実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// src/hooks/useLocalStorage.ts import { useState, useEffect } from "react"; /** * ローカルストレージと同期した state を取得する。 * * @param key ストレージキー * @param initialValue 初期値(関数でも可) * @returns [value, setValue] のタプル */ export function useLocalStorage<T>(key: string, initialValue: T | (() => T)) { // 初回レンダー時にローカルストレージから復元 const [value, setValue] = useState<T>(() => { if (typeof window === "undefined") return typeof initialValue === "function" ? (initialValue as () => T)() : initialValue; try { const stored = window.localStorage.getItem(key); return stored ? (JSON.parse(stored) as T) : (typeof initialValue === "function" ? (initialValue as () => T)() : initialValue); } catch { // パースエラーやアクセス例外はデフォルトにフォールバック return typeof initialValue === "function" ? (initialValue as () => T)() : initialValue; } }); // state が変化したらローカルストレージへ保存 useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(value)); } catch { // 書き込み失敗は無視(容量超過等) } }, [key, value]); return [value, setValue] as const; } |
使用例:テーマ切替
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { useLocalStorage } from "./hooks/useLocalStorage"; export function ThemeSwitcher() { const [theme, setTheme] = useLocalStorage<"light" | "dark">("app-theme", "light"); return ( <button onClick={() => setTheme(prev => (prev === "light" ? "dark" : "light"))} > 現在: {theme} </button> ); } |
テストポイント
| 項目 | 方法 |
|---|---|
| 初期化時にローカルストレージが読み込まれるか | jest.spyOn(window.localStorage, "getItem") をモックし、期待値を検証 |
| 更新時に正しい JSON が保存されるか | setValue 後の localStorage.setItem 呼び出し回数と引数をチェック |
window が未定義の場合(SSR)でも例外が起きないか |
Node 環境でフックを呼び出し、エラーが投げられないことを確認 |
useFormPersist の実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// src/hooks/useFormPersist.ts import { useState, useEffect } from "react"; type ChangeHandler<T> = (field: keyof T) => ( e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> ) => void; /** * フォームの入力値をローカルストレージに自動保存するフック。 * * @param key 保存先キー * @param initialValues 初期フォームオブジェクト */ export function useFormPersist<T extends Record<string, any>>( key: string, initialValues: T ) { const [values, setValues] = useState<T>(() => { try { const saved = window.localStorage.getItem(key); return saved ? (JSON.parse(saved) as T) : initialValues; } catch { return initialValues; } }); // デバウンス付き保存(300ms) useEffect(() => { const timer = setTimeout(() => { try { window.localStorage.setItem(key, JSON.stringify(values)); } catch {} }, 300); return () => clearTimeout(timer); }, [key, values]); const handleChange: ChangeHandler<T> = field => e => { const v = e.target.value; setValues(prev => ({ ...prev, [field]: v })); }; return { values, handleChange }; } |
使用例:お問い合わせフォーム
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import { useFormPersist } from "./hooks/useFormPersist"; export function ContactForm() { const { values, handleChange } = useFormPersist("contact-form", { name: "", email: "", message: "" }); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); // 例:POST to API await fetch("/api/contact", { method: "POST", body: JSON.stringify(values), headers: { "Content-Type": "application/json" } }); window.localStorage.removeItem("contact-form"); // 完了後は削除 }; return ( <form onSubmit={onSubmit}> <input placeholder="名前" value={values.name} onChange={handleChange("name")} /> <input placeholder="メール" type="email" value={values.email} onChange={handleChange("email")} /> <textarea placeholder="メッセージ" value={values.message} onChange={handleChange("message")} /> <button type="submit">送信</button> </form> ); } |
テスト戦略
- ユニットテスト:
@testing-library/react-hooks(またはreact-hooks-testing-library)でlocalStorageをモックし、handleChangeが state と storage の両方に反映されるか検証。 - 統合テスト:実際のコンポーネントをレンダリングし、
window.location.reload()後に入力内容が復元されていることを確認。
フック選択ガイドライン
| シナリオ | 推奨フック / パターン | 理由 |
|---|---|---|
| 単一 API 呼び出し(結果が UI に直結) | use(fetch…) + <Suspense>(実験的) |
Promise がサスペンドされ、ローディングコード不要 |
| 複数リクエストや条件分岐がある場合 | カスタムフック内で useEffect + fetch または TanStack Query |
依存関係管理とキャッシュ戦略を柔軟にコントロールできる |
| データ取得後の加工・バリデーション | カスタムフック(内部で use() と useState を併用) |
use() が提供する「取得」部分だけを抽象化し、残りは通常の状態ロジックで処理 |
| ローカルストレージへの永続化 | useLocalStorage, useFormPersist など独立したカスタムフック |
UI ロジックから永続化ロジックを分離し、再利用性とテスト容易性を向上 |
| リアルタイム通信(WebSocket, SSE) | useEffect + useRef |
サスペンドは不要で、クリーンアップが必須になるため |
ローカル開発環境での検証手順
- React 19 α プロジェクトを作成
bash
npx create-react-app my-app --template typescript
cd my-app
# React 19 alpha をインストール(2026‑04 時点ではまだ α と仮定)
npm install react@19.0.0-alpha react-dom@19.0.0-alpha
src/App.tsxにサンプルコードを貼り付けUserProfile(use() デモ)ThemeSwitcher(useLocalStorage デモ)-
必要に応じて
ErrorBoundary,RetryButtonも同梱 -
エントリポイントでラップ
tsx
// src/index.tsx
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ErrorBoundary } from "./components/ErrorBoundary";
ReactDOM.createRoot(document.getElementById("root")!).render(