Next.js App Router よくある落とし穴と解決策:遠回りを減らす 8 つの実践知見
Next.js App Router を使い始めた当初、本当にたくさんの落とし穴にハマりました。
昨年末、会社のプロジェクトを Next.js 15 にアップグレードすることになり、せっかくなら Pages Router から App Router へ移行しようと決めました。公式ドキュメントには「より高速なパフォーマンス」「より良い開発体験」「Server Components による革新的なアーキテクチャ」と書かれていました。ところが、初日から奇妙な問題が次々と出てきたのです。
データが更新されない、ページがずっとローディングのまま、キャッシュを設定したのに効かない、Server Component と Client Component の区別がつかない——。中でも最悪だったのは、error.tsx に 'use client' を書き忘れたせいで 3 時間もデバッグしたことです。あのときの気持ちは、本当に言葉になりません。
その後、チーム内で集計してみると、みんなが直面した問題の 80% は同じものでした。そこで、実際に踏んだ落とし穴を整理し、同じ轍を踏まないように共有することにしました。
本記事では理論より実践にフォーカスします。各問題について、なぜハマるのか、どう気づくか、どう解決するかを説明します。読み終わる頃には、App Router の落とし穴の避け方が身についているはずです。
データ取得の落とし穴
落とし穴 1:クライアントで重複データ取得
状況再現:
ユーザー情報をページに表示する必要があり、いつもの癖でこんなコードを書きました。
// app/profile/page.tsx
'use client'
import { useEffect, useState } from 'react'
export default function ProfilePage() {
const [user, setUser] = useState(null)
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => setUser(data))
}, [])
if (!user) return <div>Loading...</div>
return <div>Hello, {user.name}</div>
}
一見問題なさそうですが、典型的なアンチパターンです。データは DB → API Route → クライアントと、不要なネットワーク往復が 1 回増えています。
なぜハマるのか:
Pages Router 時代は useEffect でクライアント取得するのが当たり前でした。App Router の Server Components なら、サーバー側で直接データを取れます。API 層は不要です。
正しいやり方:
// app/profile/page.tsx(デフォルトは Server Component)
import { db } from '@/lib/db'
export default async function ProfilePage() {
// サーバー側で DB を直接クエリ
const user = await db.user.findFirst()
return <div>Hello, {user.name}</div>
}
パフォーマンスはすぐに改善されます。
- API リクエストが 1 回減る
- サーバーから DB へのレイテンシは通常 10ms 未満(クライアントからサーバーへは 100ms 以上かかることも)
- クライアントの JavaScript バンドルが小さくなる
要点:
Server Component で取得できるデータは、わざわざクライアントで fetch しない。ユーザー操作(検索、フィルタ、リアルタイム更新)が必要なときだけクライアント取得を検討する。
落とし穴 2:Route Handler のデフォルトキャッシュ
状況再現:
現在時刻を返す API を書いたのに、何度リロードしても時刻が変わりません。
// app/api/time/route.ts
export async function GET() {
return Response.json({ time: new Date().toISOString() })
}
10 回リロードしても同じ時刻。コードが反映されていないのかと疑いました。
なぜハマるのか:
Next.js は GET リクエストの Route Handler をデフォルトでキャッシュします。設定情報のような静的データには都合がいいですが、動的データには向きません。
解決策 1:動的であることを明示
// app/api/time/route.ts
export const dynamic = 'force-dynamic' // 強制的に動的レンダリング
export async function GET() {
return Response.json({ time: new Date().toISOString() })
}
解決策 2:Next.js 15 の新しいデフォルト
Next.js 15 では GET Route Handler のデフォルトがキャッシュなしに変わりました。Next.js 14 を使っている場合は、次のように書けます。
// app/api/time/route.ts
export async function GET() {
return Response.json(
{ time: new Date().toISOString() },
{ headers: { 'Cache-Control': 'no-store' } }
)
}
私の運用ルール:
今の私の習慣はこうです。
- 静的データ(設定、定数):
export const revalidate = 3600を明示 - 動的データ(ユーザー情報、リアルタイムデータ):
export const dynamic = 'force-dynamic'を明示
デフォルト挙動に頼らず、意図をコードに書く。これが一番わかりやすいです。
落とし穴 3:データ変更後の再検証を忘れる
状況再現:
シンプルな Todo アプリを作ったのに、新しいタスクを追加してもリストが更新されません。
// app/todos/page.tsx
export default async function TodosPage() {
const todos = await db.todo.findMany()
return <TodoList todos={todos} />
}
// app/actions.ts
'use server'
export async function addTodo(text: string) {
await db.todo.create({ data: { text } })
// 再検証を忘れている!
}
フォーム送信後も古いデータのまま。手動リロードしないと新しいタスクが見えません。
なぜハマるのか:
App Router のキャッシュは積極的です。データが変わっても、ページは自動更新されません。「このパスのデータが古くなった」と明示的に伝える必要があります。
正しいやり方:
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function addTodo(text: string) {
await db.todo.create({ data: { text } })
revalidatePath('/todos') // /todos パスを再検証
}
応用テクニック:
Todo リストを複数ページ(トップ、アーカイブなど)で表示しているなら、revalidateTag の方が柔軟です。
// app/todos/page.tsx
export default async function TodosPage() {
const todos = await fetch('http://localhost:3000/api/todos', {
next: { tags: ['todos'] } // データにタグを付ける
})
return <TodoList todos={todos} />
}
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function addTodo(text: string) {
await db.todo.create({ data: { text } })
revalidateTag('todos') // 'todos' タグ付きデータをすべて再検証
}
要点:
データ更新の三ステップ:書き込み →
revalidatePath/revalidateTag→ リダイレクト(任意)
Server Components と Client Components の混乱
落とし穴 4:Server Component で Context を使う
状況再現:
テーマ切り替えをグローバルに提供したくて、ThemeProvider を書きました。
// app/providers.tsx
import { createContext } from 'react'
export const ThemeContext = createContext('light')
export function Providers({ children }) {
return (
<ThemeContext.Provider value="dark">
{children}
</ThemeContext.Provider>
)
}
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
エラーが出ました:You're importing a component that needs createContext. This only works in a Client Component.
なぜハマるのか:
Server Components は React Context をサポートしません。サーバーでレンダリングされるため、クライアント側の状態管理機構がありません。
正しいやり方:
Provider は Client Component にして、別ファイルに切り出します。
// app/providers.tsx
'use client' // 重要:Client Component としてマーク
import { createContext, useState } from 'react'
export const ThemeContext = createContext('light')
export function Providers({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
// app/layout.tsx(Server Component のまま)
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
私が踏んだ落とし穴:
最初は 'use client' を layout.tsx に付けてしまいました。アプリ全体が Client Component になり、Server Components のメリットが消えてしまったのです。Provider だけ Client Component にし、Layout は Server Component のまま——これを覚えておいてください。
落とし穴 5:Client Component の SSR 誤解
状況再現:
Client Component で localStorage を使いました。ローカル開発では問題ないのに、デプロイ後に localStorage is not defined エラーが出ました。
// app/components/user-info.tsx
'use client'
export default function UserInfo() {
const user = JSON.parse(localStorage.getItem('user') || '{}')
return <div>{user.name}</div>
}
なぜハマるのか:
'use client' は「クライアントだけで動く」という意味ではありません。Client Components もサーバーでプリレンダリング(SSR)されます。localStorage はブラウザにしかなく、サーバーからアクセスすると当然エラーになります。
解決策 1:環境をチェック
'use client'
import { useEffect, useState } from 'react'
export default function UserInfo() {
const [user, setUser] = useState(null)
useEffect(() => {
// useEffect はクライアントでのみ実行される
const userData = JSON.parse(localStorage.getItem('user') || '{}')
setUser(userData)
}, [])
if (!user) return null
return <div>{user.name}</div>
}
解決策 2:条件分岐
'use client'
export default function UserInfo() {
const user = typeof window !== 'undefined'
? JSON.parse(localStorage.getItem('user') || '{}')
: null
if (!user) return null
return <div>{user.name}</div>
}
要点:
Client Component = クライアントでインタラクションできるコンポーネント。それでもサーバーでプリレンダリングされる。ブラウザ API(localStorage、window、document)は
useEffect内か、環境チェック後に使う。
落とし穴 6:'use client' の使いすぎ
状況再現:
App Router を始めた頃、エラーが出るたびに 'use client' を付ける癖がつき、プロジェクト全体が Client Components だらけになりました。Server Components のメリットがほぼ消えていました。
なぜハマるのか:
Client Component が必要に見える問題の多くは、コードの分割の問題にすぎません。
悪い例:
// app/dashboard/page.tsx
'use client' // 付けるべきではない!
import { useState } from 'react'
export default function Dashboard() {
const [count, setCount] = useState(0)
return (
<div>
<Header /> {/* 静的 */}
<Stats /> {/* サーバーデータが必要 */}
<Counter count={count} setCount={setCount} /> {/* インタラクションが必要 */}
</div>
)
}
こう書くとページ全体が Client Component になり、Stats のデータもクライアント fetch になります。
正しいやり方:
// app/dashboard/page.tsx(Server Component)
import { db } from '@/lib/db'
import { Counter } from './counter'
export default async function Dashboard() {
const stats = await db.stats.findFirst() // サーバーでデータ取得
return (
<div>
<Header /> {/* Server Component */}
<Stats data={stats} /> {/* Server Component */}
<Counter /> {/* Client Component */}
</div>
)
}
// app/dashboard/counter.tsx
'use client' // このコンポーネントだけ Client Component
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
私の判断基準:
'use client' が必要かどうか、3 つで判断します。
- React Hooks(useState、useEffect、useContext など)を使う
- ブラウザイベント(onClick、onChange など)をリッスンする
- ブラウザ API(localStorage、window など)を使う
この 3 つに当てはまらなければ、Server Component のままにします。
キャッシュの落とし穴
落とし穴 7:Client Router Cache の混乱
状況再現:
ユーザーが /posts/1 で記事を編集し、保存後 /posts 一覧に戻ると、タイトルが古いまま。ページをリロードすると更新されます。
なぜハマるのか:
App Router には Client Router Cache(クライアントルートキャッシュ)があり、訪問済みページをキャッシュします。データが更新されても、遷移時には古いキャッシュが表示されることがあります。
解決策 1:遷移時にリフレッシュ
// app/posts/[id]/edit/page.tsx
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function updatePost(id: string, title: string) {
await db.post.update({ where: { id }, data: { title } })
revalidatePath('/posts') // 一覧ページを再検証
revalidatePath(`/posts/${id}`) // 詳細ページを再検証
redirect('/posts') // 一覧へ戻る
}
解決策 2:router.refresh() を使う
'use client'
import { useRouter } from 'next/navigation'
export function EditForm() {
const router = useRouter()
async function handleSubmit() {
await updatePost(...)
router.refresh() // 現在ルートのデータをリフレッシュ
router.push('/posts')
}
}
Next.js 15 の朗報:
Next.js 15 では Client Router Cache のデフォルトがキャッシュなしに変わりました。この問題はほぼ気にしなくてよくなります。Next.js 14 なら、手動で対処してください。
落とし穴 8:revalidate が効かない
状況再現:
Server Component で revalidate = 60 を設定し、60 秒ごとにデータが更新されるはずだったのに、ずっと変わりません。
// app/news/page.tsx
export const revalidate = 60 // 60 秒後に再生成を期待
export default async function NewsPage() {
const news = await fetch('https://api.example.com/news')
return <NewsList news={news} />
}
デプロイ後、ニュース一覧が丸一日更新されませんでした。
なぜハマるのか:
revalidate は本番環境でのみ有効です。開発環境(npm run dev)ではキャッシュされません。また、静的生成されたページにのみ効き、動的レンダリングと判定されたページでは無効になります。
確認手順:
- 本番環境で確認:
npm run build
npm run start
- ページが静的か確認:
ビルド出力で ○ Static または ● SSG になっているか確認。λ Dynamic なら動的レンダリングです。
- 動的レンダリングの原因を特定:
よくある原因:
cookies()やheaders()を使っているsearchParams(動的ルートパラメータ)を使っている- Route Handler に
revalidateが設定されていない
解決策:
// app/news/page.tsx
export const revalidate = 60
export default async function NewsPage() {
const news = await fetch('https://api.example.com/news', {
next: { revalidate: 60 } // fetch レベルの revalidate
})
return <NewsList news={news} />
}
私の運用ルール:
- 純粋な静的コンテンツ:
generateStaticParams+revalidate - 動的パラメータが必要:ISR(Incremental Static Regeneration)
- リアルタイムデータ:
dynamic = 'force-dynamic'を付け、revalidateは使わない
エラー処理の落とし穴
落とし穴 9:error.tsx に 'use client' を付け忘れる
状況再現:
error.tsx を作ってページエラーを処理しようとしたら、エラー:ReactServerComponentsError: Client Component must be used in a Client Component boundary.
// app/error.tsx(誤った書き方)
export default function Error({ error, reset }) {
return (
<div>
<h2>エラーが発生しました!</h2>
<button onClick={reset}>再試行</button>
</div>
)
}
なぜハマるのか:
error.tsx は Client Component である必要があります。React の Error Boundary 機構を使うためで、Error Boundary はクライアントでのみ動作します。
正しい書き方:
// app/error.tsx
'use client' // 必須
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>エラーが発生しました!</h2>
<p>{error.message}</p>
<button onClick={reset}>再試行</button>
</div>
)
}
要点:
error.tsx、loading.tsx、not-found.tsxなどの特殊ファイルのうち、Client Component が必須なのはerror.tsxだけ。他は Server Component でも構いません。
落とし穴 10:try/catch 内での redirect の位置
状況再現:
Server Action でフォーム検証を行い、失敗時にエラーページへリダイレクトしようとしたら、redirect が catch に捕捉されて失敗しました。
// app/actions.ts(誤った書き方)
'use server'
import { redirect } from 'next/navigation'
export async function createUser(data: FormData) {
try {
const user = await db.user.create({ data })
redirect(`/users/${user.id}`) // ここが catch に捕捉される!
} catch (error) {
console.error(error)
return { error: 'Failed to create user' }
}
}
なぜハマるのか:
redirect() は特殊なエラーを throw して実装されています。Next.js がそれを捕捉してリダイレクトを実行します。try/catch 内で redirect すると、あなたの catch が先に捕捉してしまい、リダイレクトが失敗します。
正しい書き方:
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
export async function createUser(data: FormData) {
try {
const user = await db.user.create({ data })
// ここでは redirect しない
return { success: true, userId: user.id }
} catch (error) {
console.error(error)
return { error: 'Failed to create user' }
}
}
// 呼び出し側で redirect
export async function handleSubmit(data: FormData) {
const result = await createUser(data)
if (result.success) {
redirect(`/users/${result.userId}`) // try/catch の外で
}
}
別パターン:
'use server'
import { redirect } from 'next/navigation'
export async function createUser(data: FormData) {
try {
const user = await db.user.create({ data })
} catch (error) {
console.error(error)
return { error: 'Failed to create user' }
}
redirect(`/users/${user.id}`) // try/catch の後
}
移行時の落とし穴
落とし穴 11:404.js と 500.js が使えなくなった
状況再現:
Pages Router から移行する際、pages/404.js と pages/500.js を残していましたが、どちらも効きませんでした。
なぜハマるのか:
App Router ではエラー処理の仕組みが完全に変わりました。
404.js→not-found.tsx500.js→error.tsx- グローバルエラー →
global-error.tsx
正しいやり方:
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - ページが見つかりません</h2>
<Link href="/">ホームに戻る</Link>
</div>
)
}
// app/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>500 - サーバーエラー</h2>
<p>{error.message}</p>
<button onClick={reset}>再試行</button>
</div>
)
}
// app/global-error.tsx(ルートレイアウトのエラーを捕捉)
'use client'
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>グローバルエラー</h2>
<p>{error.message}</p>
<button onClick={reset}>再試行</button>
</body>
</html>
)
}
落とし穴 12:next-seo が使えなくなった
状況再現:
プロジェクトで next-seo を SEO メタデータ管理に使っていましたが、App Router に移行したら効かなくなりました。
// pages/blog/[slug].tsx(Pages Router 時代)
import { NextSeo } from 'next-seo'
export default function BlogPost({ post }) {
return (
<>
<NextSeo
title={post.title}
description={post.excerpt}
openGraph={{
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
}}
/>
<article>{post.content}</article>
</>
)
}
なぜハマるのか:
App Router にはネイティブの generateMetadata API があり、next-seo は非推奨になりました。
移行方法:
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
メリット:
- 型安全(TypeScript サポート)
- async/await 対応(DB を直接クエリできる)
- パフォーマンス向上(サーバーサイドレンダリング)
パフォーマンス最適化の提案
不要な Client Components を避ける
問題:ページ全体が Client Component になり、Server Components のメリットが失われる。
解決策:
「リーフノード Client Components」戦略を採用します。
// ❌ 悪い例
// app/dashboard/page.tsx
'use client'
export default function Dashboard() {
return (
<div>
<Header />
<Sidebar />
<MainContent />
<Footer />
</div>
)
}
// ✅ 良い例
// app/dashboard/page.tsx(Server Component)
import { Header } from './header'
import { Sidebar } from './sidebar'
import { MainContent } from './main-content'
import { Footer } from './footer'
export default function Dashboard() {
return (
<div>
<Header /> {/* Server Component */}
<Sidebar /> {/* Client Component(インタラクション) */}
<MainContent /> {/* Server Component */}
<Footer /> {/* Server Component */}
</div>
)
}
// app/dashboard/sidebar.tsx
'use client' // これだけ Client Component
export function Sidebar() {
const [collapsed, setCollapsed] = useState(false)
return <aside>...</aside>
}
Suspense 境界の最適化
問題:ページ全体が遅いデータを待ち、初回表示が白画面のまま長引く。
解決策:
<Suspense> で読み込みを分割します。
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { FastComponent } from './fast'
import { SlowComponent } from './slow'
export default function Dashboard() {
return (
<div>
{/* 速いデータはすぐ表示 */}
<FastComponent />
{/* 遅いデータはスケルトンを表示 */}
<Suspense fallback={<div>読み込み中...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
並列データ取得を活用する
問題:データを直列取得すると、合計時間 = すべてのリクエスト時間の合計になる。
解決策:
// ❌ 直列取得(遅い)
export default async function Page() {
const user = await getUser() // 100ms
const posts = await getPosts() // 200ms
const comments = await getComments() // 150ms
// 合計:450ms
}
// ✅ 並列取得(速い)
export default async function Page() {
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
])
// 合計:200ms(最も遅いリクエストに依存)
}
開発環境の落とし穴
落とし穴 13:ホットリロードによる接続リーク
状況再現:
開発環境をしばらく動かすと、DB が too many connections エラーを出しました。
なぜハマるのか:
ホットリロード(Hot Reload)はモジュールコードを再実行します。グローバルに DB 接続を作ると、リロードのたびに新しい接続が作られ、古い接続は閉じられません。
解決策:
// lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ['query'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
開発環境ではホットリロード時に同じ Prisma インスタンスを再利用できます。
落とし穴 14:開発サーバーがだんだん遅くなる
状況再現:
npm run dev を 30 分ほど動かすと、ホットリロードが非常に遅くなり、ときどきフリーズします。
なぜハマるのか:
App Router の開発サーバーは、ページ数が多いと特にメモリを大量に消費します。動的ルートが多いほど顕著です。
一時的な対処:
- 開発サーバーを再起動(根本解決にはならない)
- 不要なファイル監視を減らす:
// next.config.js
module.exports = {
webpack: (config) => {
config.watchOptions = {
poll: 1000, // 1 秒ごとにファイル変更をチェック(頻度を下げる)
aggregateTimeout: 300,
ignored: /node_modules/,
}
return config
},
}
長期的な解決策:
Next.js 15 にアップグレードし、Turbopack を使います。
npm run dev --turbo
Turbopack のホットリロードは 10 倍以上速く、大規模プロジェクトでも快適です。
まとめ:落とし穴回避チェックリスト
ここまで多くの落とし穴を見てきました。新プロジェクトを始める前にこのリストを確認すれば、90% の問題を避けられます。
データ取得
- ☑ Server Component で取得できるデータは、Client Component + useEffect を使わない
- ☑ Route Handler に
dynamic = 'force-dynamic'またはrevalidateを明示 - ☑ データ変更後は
revalidatePath/revalidateTagを忘れない
Server / Client Components
- ☑ Provider は Client Component、Layout は Server Component のまま
- ☑ ブラウザ API(localStorage、window)は
useEffect内か環境チェック後に使う - ☑ 本当にインタラクションが必要なコンポーネントだけ
'use client'を付ける。ページ全体に付けない
エラー処理
- ☑
error.tsxには必ず'use client'を付ける - ☑
redirectは try/catch の中に書かない - ☑ ルートレイアウトのエラーは
global-error.tsxで処理(error.tsxではない)
キャッシュ
- ☑ Next.js 15 にアップグレードし、より合理的なデフォルトキャッシュを活用
- ☑ revalidate は本番環境でのみ有効。開発環境では依存しない
- ☑ 動的ページでは revalidate を使わず、
dynamic = 'force-dynamic'を使う
移行関連
- ☑
404.js→not-found.tsx、500.js→error.tsx - ☑
next-seo→generateMetadata - ☑
getServerSideProps→ Server Component で直接 fetch - ☑
useRouterはnext/routerからnext/navigationへ
パフォーマンス最適化
- ☑ Suspense で速い/遅いコンポーネントを分割
- ☑ データ取得は並列化(
Promise.all) - ☑ 開発環境では DB 接続をシングルトンにする
- ☑ Turbopack を使う(
npm run dev --turbo)
最後に
App Router には確かに学習曲線があります。最初は落とし穴にハマるのが普通です。でも、これらのパターンを身につければ、開発効率は確実に上がります。
今の私の習慣はこうです。
- 新機能はまずデータフローを考える:サーバーレンダリングが必要か、クライアントインタラクションか?
- 問題が出たらまずビルド出力を見る:Static か Dynamic か?なぜ?
- DevTools を活用:Network パネルでリクエスト数、Console でエラースタック
- デフォルト挙動に頼らない:キャッシュ、レンダリング方式、再検証の意図を明示する
一番大事なのは、これらの落とし穴を恐れず、実際に手を動かすこと。1 回踏めば、次からは覚えています。Next.js App Router の公式ドキュメントも詳しいので、困ったらドキュメントを見れば、だいたい答えがあります。
この記事が役に立ったら、同じ轍を踏んでいる友人にもシェアしてください。新しい落とし穴があればコメントで教えてください。リストは随時更新していきます。
App Router の道で、遠回りを減らし、よりエレガントなコードを書けることを祈っています!
FAQ
Server Component と Client Component はどう使い分ける?
• サーバーで実行され、クライアントには送られない
• useState、useEffect などの Hooks は使えない
• ブラウザ API は使えない
Client Component(明示が必要):
• `'use client'` ディレクティブでマークする
• すべての React Hooks が使える
• ブラウザ API が使える
判断基準:インタラクションやブラウザ API が必要なら Client Component
データが更新されないのはなぜ?
解決策:
• cache: 'no-store' を設定(リクエストごとに最新データを取得)
• next: { revalidate: 60 } を使う(60 秒後に再検証)
• Client Component で router.refresh() を呼び、強制リフレッシュ
確認方法:ビルド出力でページが Dynamic か Static かを確認
ページがずっとローディングのままになる
• 非同期 Server Component で loading 状態を正しく処理していない
• Suspense 境界の設定ミス
• データ取得失敗時のエラー処理がない
対処法:
• loading.tsx を追加
• 非同期コンポーネントを Suspense でラップ
• error.tsx でエラーを処理
error.tsx が効かない
必ず `'use client'` を追加:
'use client'
export default function Error({ error, reset }) {
return <div>エラー:{error.message}</div>
}
注意:error.tsx は子コンポーネントのエラーしか捕捉できず、自身のエラーは捕捉できません
Pages Router から App Router へどう移行する?
• getServerSideProps → 非同期 Server Component
• getStaticProps → 静的生成(デフォルト)
• next/router → next/navigation
• _app.js → layout.tsx
• _document.js → 不要(layout.tsx が担当)
推奨:まず 1〜2 ページで試し、流れを確認してから本格移行
キャッシュ機構はどう理解すればいい?
• Request Memoization:同一リクエスト内で同じ fetch は 1 回だけ実行
• Data Cache:fetch のレスポンスがキャッシュされる
• Full Route Cache:ページ全体がキャッシュされる(静的生成)
• Router Cache:クライアント側のルートキャッシュ
制御方法:cache: 'no-store'、next: { revalidate } などのオプションを使う
App Router の問題はどうデバッグする?
• ビルド出力(npm run build)でページタイプを確認
• DevTools の Network パネルでリクエストを確認
• Console のエラーメッセージを確認
• Next.js ターミナル出力を確認
よくある問題:
• Static なのに Dynamic であるべき → fetch のキャッシュ設定を確認
• データが更新されない → キャッシュと revalidate 設定を確認
• ページがローディングのまま → loading.tsx と Suspense を確認
5分で読めます · 公開日: 2025年12月25日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js 動的ルーティングとパラメータ処理の完全ガイド:入門から型安全まで
Next.js 14+ の動的ルーティングを手取り足取り解説。動的パラメータ、catch-all ルート、オプショナルパラメータ、generateStaticParams の使いどころ、TypeScript による型安全な実装まで。パラメータ取得方法の変化に迷った人向けに、実戦コード例を多数掲載。
第 10 / 47 記事
次の記事
Next.js SSR vs SSG vs ISR:レンダリング戦略の選び方ガイド
Next.js で SSR・SSG・ISR のどれを使うべきか迷っていませんか? 実践シナリオの比較と判断フローで最適なレンダリング戦略を素早く選べます。ISR が効かない、初期表示が遅いといったよくある問題の解決策も紹介します。
第 12 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
コメント
GitHubアカウントでログインしてコメントできます