0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js 15 & React 19 完全マイグレーションガイド(背景解説付き)

Posted at

この記事でわかること

  • 変更点の具体例(Before/After) をコードで確認
  • なぜ(背景・意図)」まで踏み込んだ解説
  • 重要度 / 緊急度 ラベルで優先順位を可視化

🚀 概要

Next.js 15(15.3.5)と React 19(19.1.0)は Server Components+ストリーミング時代を前提とした大刷新 となっています。
安定版がリリースされたため、本格的な移行を検討するタイミングになりました。
とくに Async Request APIsキャッシュ no-store デフォルト化 はほぼ全ページに影響するため、早期対応が必要です。
また、React 19 の 新しい use() と Actions フック群 によりデータ取得とフォーム送信の DX が統一されます。
この記事では Why → How → Before/After の順で整理しているので、各プロジェクトの状況に合わせて移行を進める際の参考にしていただけます。


Next.js 15 の主要変更

🚨 1. Async Request APIs(重要度: ★★★ 緊急度: ★★★)

Before (v14) After (v15)
同期アクセス const id = params.id const { id } = await params
// ✅ v15 の正しい書き方
type Params = Promise<{ id: string }>

export default async function Page({ params }: { params: Params }) {
  const { id } = await params
  /* ... */
}

Why: React 19 の use() と統一し、「await =動的レンダリング」を型で明示するようになりました。これにより、Edge/Node をまたいで I/O 処理を一貫して扱えるようになります。

How: await params, await cookies() への書き換えが必要です。npx @next/codemod upgrade latest で自動変換も可能です。


🚨 2. デフォルトキャッシュが no-store に変更(重要度: ★★★ 緊急度: ★★☆)

Before After
fetch() デフォルト force-cache no-store
GET Route Handler 自動キャッシュ 非キャッシュ

*v14.2 以降は暗黙的に force-cache 相当

// 明示的にキャッシュしたい場合
await fetch(url, {
  cache: 'force-cache',
  next: { revalidate: 60 } // 再検証秒数を明示
});
export const dynamic = 'force-static'

Why:

  1. Partial Prerendering との組み合わせで古い HTML が残る事故を防げるようになります
  2. 暗黙キャッシュはバグの温床になりがちなので、意図をコードに明示することで可読性が向上します

How: API/Route ごとに cache オプションや dynamic 設定を明示する必要があります。unstable_noStore() で一括 opt-out することも可能です。


🚨 3. ESLint 9(Flat Config)サポート(重要度: ★★☆ 緊急度: ★★☆)

// eslint.config.js で Flat Config に移行例
import next from 'eslint-plugin-next'
/* @type {import('eslint').Linter.FlatConfig[]} */
export default [
  ...next.configs['core-web-vitals'],
  {
    rules: { /* … */ },
  },
]

Why: ESLint 8 は 2024-10-05 に EOL を迎えるため、新 API に移行しないと今後ルール追加が対応されなくなります。

How: .eslintrc.* から eslint.config.js への移行が必要です。ESLINT_USE_FLAT_CONFIG=false を残しつつ段階的に移行することをお勧めします。


✨ 4. Turbopack for Development(重要度: ★★☆ 緊急度: ★☆☆)

Before (v14) After (v15)
開発サーバー Webpack Turbopack (opt-in)
起動時間 標準 -76%
HMR 標準 -96%
# opt-in: package.json
{
  "scripts": {
    "dev": "next dev --turbo",
    "build": "next build"
  }
}

Why: 開発体験の大幅な改善のためです。Rust 製バンドラにより、起動時間が76%、HMRが96%、初回ビルドが45%も高速化されます。

How: package.json の dev スクリプトに --turbo フラグを追加するだけで有効化できます。


✨ 5. Enhanced Forms (next/form)(重要度: ★★☆ 緊急度: ★☆☆)

Before (v14) After (v15)
フォーム送信 <form> タグ <Form> コンポーネント
プリフェッチ(事前読み込み) 手動実装 自動化
// ✅ v15 の新しい書き方
import { Form } from 'next/form'

<Form method="GET" action="/search">
  <input name="q" />
</Form>

Why: フォーム送信時のUX向上のためです。従来は手動でプリフェッチ(次に必要なデータを事前に読み込む機能)を実装する必要があり、開発者の実装負担が重かったためです。また、<Link> と統一された体験により、ユーザーにとって一貫性のある操作感を提供できます。

How: <Form> コンポーネントを使用することで、プリフェッチ機能が自動化されます。<form> タグを <Form> に置き換えるだけで、<Link> と同様の体験を実現できます。


✨ 6. next.config.ts 対応(重要度: ★★☆ 緊急度: ★☆☆)

Before (v14) After (v15)
設定ファイル next.config.js next.config.ts
型安全性 なし 完全対応
IDE 補完 限定的 完全対応
// next.config.ts
import type { NextConfig } from 'next'

const config: NextConfig = {
  experimental: { typedRoutes: true },
}

export default config

Why: 開発体験の向上のためです。型安全になり IDE 補完が効くようになります。また、monorepo の共通ユーティリティを import することも可能になります。

How: next.config.jsnext.config.ts にリネームし、型定義を追加するだけで移行できます。


React 19 の主要変更

✨ 1. 新しい use() フック(重要度: ★★★ 緊急度: ★★☆)

Before (React 18) After (React 19)
Promise 使用 コンポーネントトップレベル if/loop 内でも可能
ストリーミング 複雑な実装 シンプルな実装
// ✅ v19 の新しい書き方
function Post() {
  const post = use(fetch(`/api/post/1`).then(res => res.json()))
  return <h1>{post.title}</h1>
}
// ❌ v18 までの従来の書き方
function Post() {
  const [post, setPost] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetch(`/api/post/1`)
      .then(r => r.json())
      .then(data => {
        setPost(data)
        setLoading(false)
      })
      .catch(err => {
        setError(err)
        setLoading(false)
      })
  }, [])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  if (!post) return <div>No post found</div>

  return <h1>{post.title}</h1>
}

Why: Server Components のストリーミングをよりシンプルにするためです。Promise/Context を if/loop 内でも呼べるようになり、条件分岐やループ内でのデータ取得が可能になります。

How: use() フックを使用して Promise を直接渡すことで、従来の useEffectuseState を使った複雑な実装が不要になります。


✨ 2. Actions & フォーム関連フック(重要度: ★★☆ 緊急度: ★☆☆)

Before (React 18) After (React 19)
フォーム送信 手動実装 useActionState で自動化
即座UI更新(楽観的) 手動実装 useOptimistic で簡易化
送信状態 手動管理 useFormStatus で自動取得
// ✅ v19 の新しい書き方 - 統合された例
import { useActionState, useOptimistic, useFormStatus } from 'react'

// Server Action(サーバー側で実行される関数)
async function addCommentAction(prevState, formData) {
  'use server'
  
  const comment = formData.get('body')
  if (!comment) {
    return { error: 'コメントを入力してください' }
  }
  
  // データベースに保存
  await saveComment(comment)
  return { success: true }
}

function CommentForm() {
  // フォームの状態管理(pending, error, success を自動取得)
  const [formState, formAction, isPending] = useActionState(addCommentAction, {})
  
  // 即座UI更新(送信前に先に表示して、後でサーバーと同期)
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (prev, newComment) => [...prev, { ...newComment, pending: true }]
  )
  
  // フォーム送信時の処理
  const handleSubmit = (formData) => {
    const comment = formData.get('body')
    
    // 即座にUIを更新(先に表示して、後でサーバーと同期)
    addOptimisticComment({ id: Date.now(), body: comment })
    
    // 実際のサーバーアクションを実行
    formAction(formData)
  }
  
  return (
    <div>
      {/* 即座に更新されたコメント一覧(先に表示) */}
      {optimisticComments.map(comment => (
        <div key={comment.id} className={comment.pending ? 'opacity-50' : ''}>
          {comment.body}
          {comment.pending && <span>送信中...</span>}
        </div>
      ))}
      
      {/* フォーム */}
      <form action={handleSubmit}>
        <input name="body" placeholder="コメントを入力..." />
        <button disabled={isPending}>
          {isPending ? '送信中...' : '投稿'}
        </button>
      </form>
      
      {/* エラー表示 */}
      {formState.error && <div className="error">{formState.error}</div>}
      {formState.success && <div className="success">投稿しました!</div>}
    </div>
  )
}

// フォーム送信状態を取得する専用フック
function SubmitButton() {
  const { pending } = useFormStatus()
  
  return (
    <button disabled={pending}>
      {pending ? '送信中...' : '送信'}
    </button>
  )
}
// ❌ v18 までの従来の書き方
import { useState, useEffect } from 'react'

function CommentForm() {
  const [comments, setComments] = useState([])
  const [newComment, setNewComment] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [error, setError] = useState(null)
  
      // 手動で即座UI更新を実装
    const handleSubmit = async (e) => {
    e.preventDefault()
    
    if (!newComment.trim()) {
      setError('コメントを入力してください')
      return
    }
    
    setIsSubmitting(true)
    setError(null)
    
    // 即座にUIを更新(先に表示)
    const optimisticComment = { id: Date.now(), body: newComment, pending: true }
    setComments(prev => [...prev, optimisticComment])
    
    try {
      // API呼び出し
      const response = await fetch('/api/comments', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ body: newComment })
      })
      
      if (!response.ok) throw new Error('投稿に失敗しました')
      
      // 成功時:即座更新を確定
      setComments(prev => 
        prev.map(c => 
          c.id === optimisticComment.id 
            ? { ...c, pending: false }
            : c
        )
      )
      setNewComment('')
      
    } catch (err) {
      // 失敗時:即座更新を元に戻す
      setComments(prev => prev.filter(c => c.id !== optimisticComment.id))
      setError(err.message)
    } finally {
      setIsSubmitting(false)
    }
  }
  
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id} className={comment.pending ? 'opacity-50' : ''}>
          {comment.body}
          {comment.pending && <span>送信中...</span>}
        </div>
      ))}
      
      <form onSubmit={handleSubmit}>
        <input
          value={newComment}
          onChange={(e) => setNewComment(e.target.value)}
          placeholder="コメントを入力..."
        />
        <button disabled={isSubmitting}>
          {isSubmitting ? '送信中...' : '投稿'}
        </button>
      </form>
      
      {error && <div className="error">{error}</div>}
    </div>
  )
}

Why: フォーム送信のUX向上のためです。送信前の JS 未読込でもキューイングできるようになり、離脱率を低減できます。また、即座UI更新(楽観的)の実装が大幅に簡素化されます。

How: useActionStateuseOptimisticuseFormStatus などの新しいフックを使用することで、従来手動で実装していた機能が自動化されます。


✨ 3. Suspense 強化(重要度: ★★☆ 緊急度: ★☆☆)

Before (React 18) After (React 19)
ローディング表示 遅れて表示 即座に表示
関連コンポーネント 個別に処理 事前に準備
ページ表示速度 標準 改善

Why: パフォーマンス向上のためです。ローディング表示(フォールバック)を即座に表示し、関連コンポーネント(兄弟ツリー)を事前に準備(プリウォーム)する新アルゴリズムにより、ページ表示速度(TTFB)が改善されます。

How: 既存の Suspense 境界をそのまま使用できます。新しいアルゴリズムが自動的に適用されるため、特別な設定は不要です。

Suspense境界とは?
データ読み込み中の表示範囲を決める仕組みです。例えば:

<Suspense fallback={<div>読み込み中...</div>}>
  <UserProfile />  {/* この中でデータを読み込む */}
</Suspense>

React 19では、このローディング表示がより速く表示されるようになりました。


🚨 4. UMD ビルド廃止(重要度: ★★☆ 緊急度: ★★☆)

Before (React 18) After (React 19)
ビルド形式 UMD + ESM ESM のみ
CDN 読み込み <script> タグ ES Modules
ツリーシェイク 限定的 完全対応
<!-- 新しい読み込み例 -->
<script type="module">
  import React from "https://esm.run/react@19";
  import { createRoot } from "https://esm.run/react-dom@19";
</script>

Why: ビルド工程の簡素化とモダンな標準への統一のためです。テスト・リリース工程が簡素化され、ツリーシェイク可能な ESM/CDN に一本化されます。

How: UMD を使用していた場合は、ES Modules 形式に移行する必要があります。CDN を使用する場合は <script type="module"> を使用します。


🚨 5. Strict Mode の二重レンダー仕様変更(重要度: ★☆☆ 緊急度: ★☆☆)

Before (React 18) After (React 19)
メモ化された値 毎回再計算 2回目で再利用
重複処理の防止 手動で対応 自動で防止
ref の処理 毎回実行 条件付き実行

Why: 開発体験の向上とパフォーマンス改善のためです。メモ化された値(useMemo / useCallback)の結果が2回目のレンダーで再利用されるようになり、重複処理(副作用)の防止が容易になります。

How: 既存のコードは自動的に恩恵を受けます。ref で副作用を処理している場合は、再チェックをお勧めします。

// ✅ React 19 での改善例
function ExpensiveComponent({ data }) {
  // メモ化された値が2回目のレンダーで再利用される
  const expensiveValue = useMemo(() => {
    console.log('重い計算を実行中...')
    return heavyCalculation(data)
  }, [data])

  // ref の処理が条件付きで実行される
  const elementRef = useCallback((node) => {
    if (node) {
      console.log('DOM要素が設定されました')
      // 重複処理を防止
    }
  }, [])

  return <div ref={elementRef}>{expensiveValue}</div>
}

Strict Mode の二重レンダーとは?
開発時にコンポーネントを意図的に2回レンダーして、重複処理(副作用)の問題を早期発見する仕組みです。React 19では、この2回目のレンダーでメモ化された値を再利用するようになり、パフォーマンスが向上します。

重複処理(副作用)の問題とは?
開発時にコンポーネントが2回レンダーされることで、以下のような問題が発生していました:

  • 同じAPIを2回呼び出してしまう
  • 同じ計算を2回実行してしまう
  • DOM要素を2回設定してしまう

React 19では、これらの重複処理が自動的に防止されるようになりました。

なぜ2回レンダーされるのか?
Strict Modeは開発時に潜在的な問題を早期発見するため、意図的に2回レンダーします:

// 問題のあるコード例
function BadComponent() {
  // レンダリングのたびに実行される(問題)
  fetch('/api/data') // ❌ 2回レンダーで2回実行される
  
  return <div>コンポーネント</div>
}

開発時のみ2回レンダー

  • 開発時: 問題発見のため2回レンダー
  • 本番環境: パフォーマンスのため1回のみ

React 19では、この2回目のレンダーでメモ化された値を再利用するようになり、開発時のパフォーマンスも向上しました。

移行スケジュール

Phase 1 – 緊急修正(即時)

  • Dynamic Route の params 型修正
  • ビルドエラー解消
  • 基本動作確認

Phase 2 – 最適化(1週間以内)

  • キャッシュ戦略見直し
  • TypeScript 設定最適化
  • ESLint 設定更新

Phase 3 – 新機能活用(2週間以内)

  • React 19 フック導入検討
  • Suspense 境界最適化
  • next/form 活用

参考資料(公式)


まとめ

  • Async Request APIsキャッシュ戦略を最優先で対応しましょう
  • 次に ESLint 9・Turbopack で開発体験を強化します
  • 最後に React 19 の新 API を取り込んで DX を底上げしましょう
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?