React 19の`use`フック完全ガイド: Promiseとコンテキストの新しい扱い方


React 19で導入されたuseフックは、従来のHooksの制約を覆す革新的な機能です。条件分岐やループの中で呼べる、Promiseを直接読み取れるなど、これまでのReactの常識を変える機能を持っています。本記事では、useフックの仕組みと実践的な使い方を詳しく解説します。

useフックとは

useは、React 19で追加された新しいフックで、以下の特徴があります。

従来のHooksとの違い

従来のHooksの制約

function Component({ condition }: { condition: boolean }) {
  // エラー: 条件分岐の中でHooksは使えない
  if (condition) {
    const value = useContext(MyContext) // NG
  }

  // エラー: ループの中でHooksは使えない
  for (let i = 0; i < 10; i++) {
    const value = useState(0) // NG
  }
}

useの柔軟性

import { use } from 'react'

function Component({ condition }: { condition: boolean }) {
  // OK: 条件分岐の中で使える
  if (condition) {
    const value = use(MyContext) // OK
  }

  // OK: ループの中でも使える(ただし推奨されない)
  const values = []
  for (let i = 0; i < contexts.length; i++) {
    values.push(use(contexts[i])) // OK(特殊なケースのみ)
  }
}

Promiseを読み取る

useの最も革新的な機能は、Promiseを直接読み取れることです。

基本的な使い方

import { use, Suspense } from 'react'

// データフェッチ関数
async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`)
  if (!response.ok) throw new Error('Failed to fetch')
  return response.json()
}

// Promiseを受け取るコンポーネント
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  // Promiseを直接読み取る
  const user = use(userPromise)

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{user.bio}</p>
    </div>
  )
}

// 親コンポーネント
function App({ userId }: { userId: string }) {
  // Promiseを作成
  const userPromise = fetchUser(userId)

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

従来の方法との比較

// 従来の方法(useState + useEffect)
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    setLoading(true)
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false))
  }, [userId])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  if (!user) return null

  return <div>{user.name}</div>
}

// use フックを使った方法
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise)
  return <div>{user.name}</div>
}

複数のPromiseを並列で読み取る

function Dashboard({
  userPromise,
  postsPromise,
  statsPromise,
}: {
  userPromise: Promise<User>
  postsPromise: Promise<Post[]>
  statsPromise: Promise<Stats>
}) {
  // 並列で読み取る
  const user = use(userPromise)
  const posts = use(postsPromise)
  const stats = use(statsPromise)

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <Stats data={stats} />
      <PostList posts={posts} />
    </div>
  )
}

function App() {
  // 並列でフェッチ開始
  const userPromise = fetchUser('123')
  const postsPromise = fetchPosts('123')
  const statsPromise = fetchStats('123')

  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Dashboard
        userPromise={userPromise}
        postsPromise={postsPromise}
        statsPromise={statsPromise}
      />
    </Suspense>
  )
}

エラーハンドリング

import { use, Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  // Promiseがrejectされると、エラーがスローされる
  const user = use(userPromise)

  return <div>{user.name}</div>
}

function App({ userId }: { userId: string }) {
  const userPromise = fetchUser(userId)

  return (
    <ErrorBoundary
      fallback={({ error }) => (
        <div>Error: {error.message}</div>
      )}
    >
      <Suspense fallback={<div>Loading...</div>}>
        <UserProfile userPromise={userPromise} />
      </Suspense>
    </ErrorBoundary>
  )
}

コンテキストを条件付きで読み取る

基本例

import { use, createContext } from 'react'

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

function Button({ primary }: { primary?: boolean }) {
  // 条件付きでコンテキストを読み取る
  const theme = primary ? use(ThemeContext) : 'light'

  return (
    <button className={`btn btn-${theme}`}>
      Click me
    </button>
  )
}

実践的な例: 認証状態

const AuthContext = createContext<{ user: User | null }>({ user: null })

function Avatar({ showName = false }: { showName?: boolean }) {
  // showName が true の時だけ認証情報を取得
  const auth = showName ? use(AuthContext) : null

  return (
    <div className="avatar">
      <img src="/avatar.png" alt="Avatar" />
      {showName && auth?.user && <span>{auth.user.name}</span>}
    </div>
  )
}

複数コンテキストの動的選択

const LightTheme = createContext({ bg: 'white', text: 'black' })
const DarkTheme = createContext({ bg: 'black', text: 'white' })

function ThemedText({ isDark }: { isDark: boolean }) {
  // 動的にコンテキストを選択
  const theme = use(isDark ? DarkTheme : LightTheme)

  return (
    <p style={{ background: theme.bg, color: theme.text }}>
      Themed text
    </p>
  )
}

実践パターン

パターン1: データフェッチライブラリとの統合

// lib/fetcher.ts
export function createResource<T>(promise: Promise<T>) {
  let status: 'pending' | 'success' | 'error' = 'pending'
  let result: T
  let error: Error

  const suspender = promise.then(
    (data) => {
      status = 'success'
      result = data
    },
    (err) => {
      status = 'error'
      error = err
    }
  )

  return {
    read(): T {
      if (status === 'pending') throw suspender
      if (status === 'error') throw error
      return result
    },
  }
}

// 使用例
function UserProfile({ userId }: { userId: string }) {
  const userResource = createResource(fetchUser(userId))
  const user = userResource.read() // Suspense連携

  return <div>{user.name}</div>
}

パターン2: 条件付きデータフェッチ

function PostWithComments({
  postId,
  showComments,
}: {
  postId: string
  showComments: boolean
}) {
  const postPromise = fetchPost(postId)
  const post = use(postPromise)

  // showComments が true の時だけコメントを取得
  const commentsPromise = showComments ? fetchComments(postId) : null
  const comments = commentsPromise ? use(commentsPromise) : []

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>

      {showComments && (
        <section>
          <h2>Comments</h2>
          {comments.map(comment => (
            <Comment key={comment.id} comment={comment} />
          ))}
        </section>
      )}
    </article>
  )
}

パターン3: ネストされたSuspense

function UserDashboard({ userId }: { userId: string }) {
  const userPromise = fetchUser(userId)

  return (
    <div>
      {/* ユーザー情報は早く表示 */}
      <Suspense fallback={<UserSkeleton />}>
        <UserInfo userPromise={userPromise} />
      </Suspense>

      {/* 投稿リストは独立して読み込み */}
      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts userId={userId} />
      </Suspense>

      {/* 統計情報も独立して読み込み */}
      <Suspense fallback={<StatsSkeleton />}>
        <UserStats userId={userId} />
      </Suspense>
    </div>
  )
}

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

function UserPosts({ userId }: { userId: string }) {
  const posts = use(fetchPosts(userId))
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

パターン4: キャッシュとの統合

// キャッシュ付きフェッチ
const cache = new Map<string, Promise<any>>()

function cachedFetch<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
  if (cache.has(key)) {
    return cache.get(key)!
  }

  const promise = fetcher()
  cache.set(key, promise)

  // エラー時はキャッシュから削除
  promise.catch(() => cache.delete(key))

  return promise
}

// 使用例
function UserProfile({ userId }: { userId: string }) {
  const userPromise = cachedFetch(`user-${userId}`, () => fetchUser(userId))
  const user = use(userPromise)

  return <div>{user.name}</div>
}

TypeScript型定義

// use フックの型定義(参考)
function use<T>(promise: Promise<T>): T
function use<T>(context: Context<T>): T

// 実際の使用例
import { use, Context } from 'react'

type User = {
  id: string
  name: string
  email: string
}

const UserContext: Context<User | null> = createContext<User | null>(null)

function Component({ userPromise }: { userPromise: Promise<User> }) {
  // 型推論が効く
  const user = use(userPromise) // User型
  const contextUser = use(UserContext) // User | null型

  return <div>{user.name}</div>
}

パフォーマンス最適化

Promise の再生成を防ぐ

import { use, useMemo } from 'react'

function UserProfile({ userId }: { userId: string }) {
  // userIdが変わらない限り、同じPromiseを使う
  const userPromise = useMemo(
    () => fetchUser(userId),
    [userId]
  )

  const user = use(userPromise)

  return <div>{user.name}</div>
}

React Cache API(実験的)

import { cache } from 'react'

// 同一レンダリング内でキャッシュされる
const getUser = cache(async (id: string) => {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
})

function UserProfile({ userId }: { userId: string }) {
  const user = use(getUser(userId))
  return <div>{user.name}</div>
}

function UserAvatar({ userId }: { userId: string }) {
  // 同じIDなら同じPromiseが返される(重複リクエストなし)
  const user = use(getUser(userId))
  return <img src={user.avatar} alt={user.name} />
}

注意点とベストプラクティス

1. Promiseは安定した参照を渡す

// 悪い例: 毎回新しいPromiseが作られる
function BadExample({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {/* 毎レンダリングで新しいPromiseが作られる */}
      <UserProfile userPromise={fetchUser(userId)} />
    </Suspense>
  )
}

// 良い例: useMemoで安定化
function GoodExample({ userId }: { userId: string }) {
  const userPromise = useMemo(() => fetchUser(userId), [userId])

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

2. エラーバウンダリを適切に配置

function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <Suspense fallback={<Loading />}>
        <DataComponent />
      </Suspense>
    </ErrorBoundary>
  )
}

3. ローディング状態の粒度を調整

// 細かい粒度
function FinePage() {
  return (
    <>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>
      <Suspense fallback={<ContentSkeleton />}>
        <Content />
      </Suspense>
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
    </>
  )
}

// 粗い粒度(すべて揃うまで待つ)
function CoarsePage() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Header />
      <Content />
      <Sidebar />
    </Suspense>
  )
}

まとめ

useフックは、React 19の中でも特に革新的な機能です。

主な利点

  • Promiseを直接読み取れる(async/awaitライクな体験)
  • 条件分岐やループで使える柔軟性
  • Suspenseとの自然な統合
  • ボイラープレートの削減

適用場面

  • データフェッチが多いアプリケーション
  • Server Componentsとの連携
  • 段階的なローディング体験の実装

注意点

  • Promise参照の安定性に注意
  • ErrorBoundaryとSuspenseを適切に配置
  • 既存のuseEffectパターンと使い分ける

2026年現在、useフックはReactの非同期処理の新しいスタンダードとなりつつあります。従来のuseEffectベースのパターンと併用しながら、段階的に導入していくことをお勧めします。

参考リンク