Next.js Server Components データ取得完全ガイド:fetch、データベースクエリとベストプラクティス
Next.js の App Router で初めてコンポーネントを書いたとき、次のようなコードを目にしました。
async function Page() {
const data = await fetch('...')
return <div>{data}</div>
}
これだけ? いきなり await? useEffect も useState もなく、競合状態の心配もない?
クライアント向け React のやり方に慣れていると、「コンポーネントを非同期にできる」というのはルールが変わったような感覚です。さらに悩ましいのが、fetch API を使うべきか、データベースを直接叩くべきかという点です。fetch だと API 呼び出しが 1 段増える気がするし、DB を直接叩くと秘密鍵がクライアントに漏れないか不安になります。
同じような迷いがあるなら、この記事はあなた向けです。Next.js Server Components のデータ取得の正しいやり方——いつ fetch を使い、いつ DB を叩くか、async コンポーネントの書き方、キャッシュの制御、そして踏みやすい落とし穴まで整理します。
Server Components データ取得の基礎
なぜ Server Components は直接 await できるのか?
答えはシンプルです:これらはブラウザではなく、サーバー上で実行されるからです。
当たり前に聞こえるかもしれませんが、この違いが決定てきです。従来の React コンポーネントはブラウザでレンダリングされるため、データベースやファイルシステムに直接アクセスできませんでした。しかし Server Components はサーバー上で実行されるため、以前は API ルートでしかできなかったことが可能になります:
- データベースへの直接接続(Prisma, Drizzle, 生SQLなど)
- ファイルシステムの読み込み(Markdown ファイルなど)
- 内部サービスの呼び出し(CORS の心配なし)
- 環境変数や秘密鍵へのアクセス(クライアントに漏洩しない)
したがって、以下のコードは完全に安全です:
// app/posts/page.tsx
import { db } from '@/lib/db'
async function PostsPage() {
// DB を直接叩く。秘密鍵はブラウザには送信されない
const posts = await db.post.findMany()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default PostsPage
ポイント:
- コンポーネントを
async functionとして宣言する。 - データベースクエリを直接
awaitできる。 - React Hooks(
useState,useEffect)は使えない。 - デフォルトではサーバーでレンダリングされ、クライアントには生成された HTML だけが届く。
3つの主要なデータ取得方法
Server Components では、主に3つの選択肢があります:
1. fetch API
外部 API や独自の Route Handler を呼び出す最も馴染みのある方法:
async function Page() {
const res = await fetch('https://api.example.com/data')
const data = await res.json()
return <div>{data.title}</div>
}
2. データベース直接クエリ
ORM や DB クライアントを使って直接取得:
import { db } from '@/lib/db'
async function Page() {
const data = await db.posts.findFirst()
return <div>{data.title}</div>
}
3. Server Actions
主にデータの変更(フォーム送信、削除など)に使われますが、データの取得も可能です:
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title')
await db.post.create({ data: { title } })
}
では、どれを使うべきなのでしょうか?
fetch vs データベースクエリ —— 選択の基準
これは私が App Router を使い始めた時に最も悩んだ点です。結論から言うと、判断基準は明確です。
5秒で決まるデシジョンツリー
以下の3つの質問に答えてください:
- これは Server Component ですか? → YES なら次へ。NO(Client Component)なら質問3へ。
- データはどこにありますか?
- 自分のデータベース → データベース直接クエリ
- 外部 API → fetch API
- Client Component からデータを取得する必要がありますか? → Route Handler (API) を作って、そこから fetch する
シンプルですね。
なぜ「直接クエリ」を優先するのか?
Next.js 公式も推奨していますが、Server Component 内では API ルートを経由せず、直接 DB を叩くべきです。
理由は合理的です:
1. HTTP ラウンドトリップの節約
比較してみましょう:
// ❌ 遠回り:Server Component → API Route → Database
async function Page() {
const res = await fetch('/api/posts') // HTTP 呼び出し
const posts = await res.json()
return <PostList posts={posts} />
}
// ✅ 直行:Server Component → Database
async function Page() {
const posts = await db.post.findMany() // 直接取得
return <PostList posts={posts} />
}
後者の方がレイヤーが1つ少なく、応答が高速です。「微々たる差では?」と思うかもしれませんが、ネットワークのオーバーヘッドやシリアライズ/デシリアライズのコストがなくなるため、ページロードが 100-200ms 速くなることもあります。
2. 優れた型安全性
TypeScript + Prisma/Drizzle を使用している場合、直接クエリなら完全な型推論が得られます:
// エディタの補完が完璧に効く
const post = await db.post.findFirst({
include: { author: true, comments: true }
})
// post.author.name ← 型定義あり
// post.comments[0].content ← 型定義あり
fetch の場合、戻り値の型を手動で定義したり as アサーションを使う必要があり、ミスの原因になります。
3. コードが簡潔
API ルートのファイルを作る必要がなく、HTTP ステータスコードやエラーレスポンスの処理も不要です。
いつ API/fetch を使うべきか?
もちろん、API ルートが不要になるわけではありません。以下のようなケースでは必須です:
ケース1:Client Component でデータが必要な場合
ブラウザで動くコンポーネントは DB に直接アクセスできないため、API エンドポイントが必要です:
// app/api/posts/route.ts
export async function GET() {
return Response.json(await db.post.findMany())
}
// components/client-posts.tsx
'use client'
export function ClientPosts() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/posts').then(res => res.json()).then(setPosts)
}, [])
// ...
}
ケース2:外部に API を公開する場合
モバイルアプリやサードパーティ向けにデータを提供する場合。
ケース3:サードパーティサービスの利用
GitHub API や OpenAI API などは fetch で叩くしかありません。
async function Page() {
const res = await fetch('https://api.github.com/users/vercel')
// ...
}
async/await コンポーネントの正しい書き方
選び方の次は、書き方です。
基本パターン:驚くほどシンプル
最も基本的な async コンポーネントは次のとおりです。
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({
where: { id: params.id }
})
if (!product) {
return <div>Product not found</div>
}
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
</div>
)
}
loading 状態も useEffect も不要。データが返るまで待ってからレンダリングするだけ。すっきりしています。
並列 vs 直列:パフォーマンス差は大きい
ここには落とし穴があります。以前、私も踏みました。
次の 2 つ、どちらが速いと思いますか?
// ❌ 直列:遅い
async function Page() {
const user = await db.user.findFirst() // 200ms 待つ
const posts = await db.post.findMany() // さらに 150ms
const comments = await db.comment.findMany() // さらに 100ms
// 合計 450ms
return <Dashboard user={user} posts={posts} comments={comments} />
}
// ✅ 並列:速い
async function Page() {
const [user, posts, comments] = await Promise.all([
db.user.findFirst(), // 同時に発火
db.post.findMany(), // 同時に発火
db.comment.findMany(), // 同時に発火
])
// 合計 200ms(最も遅いものの時間)
return <Dashboard user={user} posts={posts} comments={comments} />
}
2 倍以上の差になることもあります。データに依存関係がなければ、必ず Promise.all で並列取得しましょう。
依存がある場合は別です。
// 直列が必須:後のクエリが前の結果に依存
async function Page({ params }) {
const user = await db.user.findUnique({ where: { id: params.id } })
// user を取ってから、その人の posts を取得
const posts = await db.post.findMany({ where: { authorId: user.id } })
return <Profile user={user} posts={posts} />
}
Suspense 境界:ローディング体験を制御する
「サーバーでデータを取っている間、ユーザーは真っ白な画面を見るのでは?」
その通りです。ただし Next.js には loading.js と Suspense があり、体験を改善できます。
方法1:loading.js ファイル
ルートフォルダに loading.tsx を置けば自動で効きます。
// app/posts/loading.tsx
export default function Loading() {
return <div>Loading posts...</div>
}
// app/posts/page.tsx
async function PostsPage() {
const posts = await db.post.findMany() // 遅いクエリ
return <PostList posts={posts} />
}
ユーザーはまず “Loading posts…” を見て、データが来たら本番 UI に置き換わります。
方法2:手動 Suspense
より細かく制御したいときは、手動で Suspense を包みます。
import { Suspense } from 'react'
async function SlowComponent() {
const data = await slowQuery() // 3秒
return <div>{data}</div>
}
async function FastComponent() {
const data = await fastQuery() // 0.5秒
return <div>{data}</div>
}
export default function Page() {
return (
<div>
<FastComponent /> {/* 速い方が先に表示 */}
<Suspense fallback={<div>Loading...</div>}>
<SlowComponent /> {/* 遅い方は待つが、上をブロックしない */}
</Suspense>
</div>
)
}
よくある間違い:Suspense の置き場所
これも私がやりました。
// ❌ 誤り:async コンポーネントの内側では Suspense が効かない
async function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
{await slowQuery()} {/* Suspense は止められない */}
</Suspense>
)
}
// ✅ 正解:async コンポーネントの外側で包む
export default function Layout() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SlowPage /> {/* async コンポーネント */}
</Suspense>
)
}
Suspense は async コンポーネントの外側に置いて初めて promise を捕捉できます。
リクエストの自動重複排除:同じ呼び出しを心配しなくてよい
もう 1 つ便利な点があります。同じリクエストを 1 回のレンダリング中に複数回呼んでも、Next.js が自動で重複排除します。
async function Header() {
const user = await db.user.findFirst() // クエリ1
return <div>{user.name}</div>
}
async function Sidebar() {
const user = await db.user.findFirst() // クエリ2だが、実際には実行されない
return <div>{user.name}</div>
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
{/* 実際の DB クエリは 1 回だけ */}
</div>
)
}
Next.js は最初の結果を覚え、以降はキャッシュから返します。複数コンポーネントで同じデータソースを呼んでも、パフォーマンスを気にしなくて大丈夫です。
キャッシュ戦略とデータの再検証
キャッシュの話になると、Next.js 15 では大きな変更があり、多くの人がハマります。
Next.js 15 のキャッシュデフォルトが変わった
以前(Next.js 14):fetch のデフォルトは cache: 'force-cache' で、ずっとキャッシュ。
いま(Next.js 15):fetch のデフォルトは cache: 'no-store' で、キャッシュせず毎回取得。
なぜ変わったか。公式の説明では、キャッシュで困っていた人が多く、「リアルタイムに更新される」と思っていたのに古いデータのまま、というケースが多かったからです。いまはデフォルトでキャッシュしない方が直感的、という判断です。
つまり、14 から 15 に上げると、ページが遅く感じることがあります。以前キャッシュされていた API が、毎回叩かれるようになるからです。
3 つのキャッシュ戦略
データの性質に応じて選びます。
1. 完全キャッシュ(静的サイト向け)
async function BlogPost({ slug }) {
const post = await fetch(`https://api.example.com/posts/${slug}`, {
cache: 'force-cache' // 永久キャッシュ(再ビルドまで)
})
return <article>{post.content}</article>
}
向き:ブログ記事、商品ページ、ドキュメント——あまり変わらないコンテンツ。
2. キャッシュなし(リアルタイムデータ)
async function StockPrice() {
const price = await fetch('https://api.example.com/stock', {
cache: 'no-store' // 毎回取得
})
return <div>Current price: {price}</div>
}
向き:株価、リアルタイムコメント、ユーザーステータス——常に最新である必要があるもの。
3. 定期再検証(ISR)
async function ProductList() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // 60 秒で期限切れ、再取得
})
return <div>{products.map(p => <Card key={p.id} {...p} />)}</div>
}
向き:商品一覧、ニューストップ——数十秒の遅れは許容するが、古すぎは困るデータ。
手動再検証:データ変更後すぐに更新
ユーザーが投稿したなど、データを変えた直後にキャッシュを更新したいときがあります。Next.js には 2 つの API があります。
1. revalidatePath(ページ全体を更新)
'use server'
import { revalidatePath } from 'next/cache'
async function createPost(formData: FormData) {
await db.post.create({ data: {...} })
revalidatePath('/posts') // /posts ページのキャッシュを更新
}
2. revalidateTag(特定タグだけ更新)
より細かい制御です。
// 取得時にタグを付ける
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] } // 'posts' タグ
})
return res.json()
}
// 必要なときにタグだけ再検証
'use server'
import { revalidateTag } from 'next/cache'
async function createPost() {
await db.post.create({ data: {...} })
revalidateTag('posts') // 'posts' タグ付きキャッシュだけ更新
}
エラー処理とパフォーマンス最適化
エラー処理:ページ全体を落とさない
Server Components でデータ取得に失敗すると、デフォルトではページ全体がクラッシュします。きちんと処理しましょう。
方法1:try/catch
async function Page() {
try {
const data = await fetch('https://api.example.com/data')
if (!data.ok) throw new Error('Failed to fetch')
return <div>{data.title}</div>
} catch (error) {
return <div>Something went wrong. Please try again.</div>
}
}
方法2:error.js ファイル
ルートフォルダに error.tsx を置くと、そのルートと子ルートのエラーを自動捕捉します。
// app/posts/error.tsx
'use client' // エラー境界は Client Component である必要がある
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
落とし穴:try/catch の中の redirect
// ❌ 誤り:redirect が投げるエラーが catch される
async function Page() {
try {
const user = await getUser()
if (!user) redirect('/login') // ここで投げられたエラーが下の catch に入る
} catch (error) {
return <div>Error</div> // redirect が効かない!
}
}
// ✅ 正解:redirect は try/catch の外
async function Page() {
let user
try {
user = await getUser()
} catch (error) {
return <div>Error</div>
}
if (!user) redirect('/login') // これなら正常にリダイレクト
}
よくある間違いと対処
私が踏んだ穴をまとめます。
間違い1:サーバー側 fetch で相対パス
// ❌ 誤り:サーバーには base URL がない
async function Page() {
const data = await fetch('/api/posts') // エラー!
}
// ✅ 正解:絶対 URL
async function Page() {
const data = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/posts`)
}
// ✅ さらに良い:DB を直接叩き、fetch しない
async function Page() {
const posts = await db.post.findMany()
}
間違い2:response.ok の確認忘れ
// ❌ 誤り:fetch は自動で例外を投げない
async function Page() {
const res = await fetch('https://api.example.com/data')
const data = await res.json() // 404 でもここまで進む
return <div>{data.title}</div>
}
// ✅ 正解:ステータスを確認
async function Page() {
const res = await fetch('https://api.example.com/data')
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
const data = await res.json()
return <div>{data.title}</div>
}
間違い3:Server Component から Route Handler を呼ぶ
// ❌ 非推奨:遠回り
async function Page() {
const res = await fetch('/api/posts') // なぜわざわざ?
const posts = await res.json()
return <PostList posts={posts} />
}
// ✅ 推奨:直接クエリ
async function Page() {
const posts = await db.post.findMany()
return <PostList posts={posts} />
}
実践例:ブログページを作る
理論はここまで。完全な例を 1 つ見ましょう。
ブログ記事詳細ページを作るとします。必要なのは次のとおりです。
- 記事本文の表示
- 著者情報の表示
- 関連記事のおすすめ
ファイル構成
app/
posts/
[slug]/
page.tsx ← 記事詳細
loading.tsx ← ローディング
error.tsx ← エラー処理
実装コード
// app/posts/[slug]/page.tsx
import { db } from '@/lib/prisma'
import { Suspense } from 'react'
import { notFound } from 'next/navigation'
// メインページ
export default async function PostPage({
params,
}: {
params: { slug: string }
}) {
// 記事と著者を並列取得
const [post, author] = await Promise.all([
db.post.findUnique({
where: { slug: params.slug },
}),
db.user.findFirst(),
])
if (!post) {
notFound() // 404 ページ
}
return (
<article>
<h1>{post.title}</h1>
<AuthorCard author={author} />
<div>{post.content}</div>
{/* 関連記事は遅くてもよいので、メインをブロックしない */}
<Suspense fallback={<div>Loading recommendations...</div>}>
<RecommendedPosts currentPostId={post.id} />
</Suspense>
</article>
)
}
// 著者カード(データは既に取得済み)
function AuthorCard({ author }) {
return (
<div>
<img src={author.avatar} alt={author.name} />
<span>{author.name}</span>
</div>
)
}
// おすすめ記事(async で独立ロード)
async function RecommendedPosts({ currentPostId }: { currentPostId: string }) {
const recommended = await db.post.findMany({
where: {
id: { not: currentPostId },
published: true,
},
take: 3,
})
return (
<div>
<h3>You might also like</h3>
{recommended.map((post) => (
<a key={post.id} href={`/posts/${post.slug}`}>
{post.title}
</a>
))}
</div>
)
}
// キャッシュ:記事は 1 時間ごとに再検証
export const revalidate = 3600
// 静的パラメータ生成(任意)
export async function generateStaticParams() {
const posts = await db.post.findMany({
select: { slug: true },
})
return posts.map((post) => ({
slug: post.slug,
}))
}
// app/posts/[slug]/loading.tsx
export default function Loading() {
return (
<div>
<div className="skeleton h-12 w-3/4" />
<div className="skeleton h-4 w-1/4 mt-4" />
<div className="skeleton h-64 mt-8" />
</div>
)
}
// app/posts/[slug]/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Failed to load post</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
設計判断の理由
- なぜ DB 直叩き? → Server Component なので API ルートを経由する必要がない
- なぜ記事と著者を並列? → 依存がないので並列の方が速い
- なぜおすすめは Suspense? → 重要度が低く、遅くてもメインを止めない
- なぜ revalidate: 3600? → 記事はあまり変わらない。1 時間キャッシュで DB 負荷を下げる
まとめ
長くなりましたが、核心は次の 4 点です。
- Server Component では DB を直接叩くのが基本。Client Component から取る必要があるとき、またはサードパーティ API のときだけ
fetch。 - async/await コンポーネントはシンプル。ただし
Promise.allで並列化し、Suspense でローディング体験を整える。 - Next.js 15 はデフォルトでキャッシュしない。データに応じて
force-cache、no-store、revalidateを選ぶ。 - エラーはきちんと処理する。
response.okを確認し、error.tsxでフォールバック。redirectは try/catch の中に入れない。
Server Components のデータ取得は、クライアントよりずっと楽です。loading 状態、競合、リクエストキャンセルを気にしなくてよい。App Router への移行を迷っているなら、この点だけでも試す価値があります。
次のプロジェクトで積極的に使ってみて、困ったらこの記事に戻ってきてください。
Next.js Server Components データ取得の完全フロー
fetch とデータベースクエリの選び方から async コンポーネント、キャッシュ戦略、エラー処理までの手順
⏱️ 目安時間: 1 時間
- 1
ステップ1: fetch とデータベースクエリの選択
データベース直接クエリ(推奨):
• より速い:API 層が 1 段少なく遅延が低い
• より安全:DB の秘密鍵がクライアントに漏れない
• 向き:DB がサーバーから到達可能なとき
コード例:
```tsx
import { db } from '@/lib/db'
export default async function Page() {
const users = await db.user.findMany()
return <div>{users.map(u => <div key={u.id}>{u.name}</div>)}</div>
}
```
fetch を使う場合:
• 向き:サードパーティ API との連携
• 向き:クロスオリジンが必要なとき
• 注意:Next.js 15 はデフォルトでキャッシュしない
コード例:
```tsx
export default async function Page() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // Next.js 15 では明示的に指定
})
const data = await res.json()
return <div>{data}</div>
}
```
選び方:まず DB 直叩き。サードパーティ API が必要なときだけ fetch - 2
ステップ2: async/await コンポーネントの書き方
async と宣言:
```tsx
export default async function Page() {
const data = await fetchData()
return <div>{data}</div>
}
```
並列取得(Promise.all):
```tsx
export default async function Page() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
])
return <div>...</div>
}
```
Suspense でローディングを最適化:
```tsx
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserList />
</Suspense>
)
}
async function UserList() {
const users = await fetchUsers()
return <div>{users.map(...)}</div>
}
```
ポイント:
• Server Components はそのまま async にできる
• Promise.all で並列取得
• Suspense でローディング体験を最適化 - 3
ステップ3: キャッシュ戦略の設定
Next.js 15 はデフォルトでキャッシュしない。明示的に指定する:
キャッシュなし(リアルタイム):
```tsx
fetch(url, { cache: 'no-store' })
```
永久キャッシュ(静的データ):
```tsx
fetch(url, { cache: 'force-cache' })
```
定期更新(ISR):
```tsx
fetch(url, { next: { revalidate: 3600 } })
```
選び方:
• リアルタイム → cache: 'no-store'
• 静的データ → cache: 'force-cache'
• 頻繁に更新するがリアルタイム不要 → revalidate
注意:Next.js 15 は「明示的であること」を重視。どのデータをキャッシュするか自分で決める。 - 4
ステップ4: エラー処理
response.ok を確認:
```tsx
const res = await fetch(url)
if (!res.ok) {
throw new Error('Failed to fetch')
}
const data = await res.json()
```
error.tsx でフォールバック:
```tsx
// app/page/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>エラー: {error.message}</h2>
<button onClick={reset}>再試行</button>
</div>
)
}
```
redirect に注意:
```tsx
// ❌ 誤り:redirect を try/catch の中に
try {
if (!user) redirect('/login')
} catch (e) {
// redirect はエラーを投げるため catch される
}
// ✅ 正解:redirect は try/catch の外
if (!user) redirect('/login')
try {
// その他の処理
} catch (e) {
// エラー処理
}
```
ポイント:
• response.ok を確認
• error.tsx でフォールバック
• redirect は try/catch の中に入れない
FAQ
Server Components はなぜ直接 await できるのか?
Server Components では次ができます:
• データベースに直接接続(Prisma、Drizzle、生 SQL)
• ファイルシステムの読み込み(Markdown など)
• 内部サービスの呼び出し(CORS を気にしなくてよい)
• 環境変数や秘密鍵へのアクセス(クライアントに漏れない)
次のコードは完全に安全です:
```tsx
import { db } from '@/lib/db'
export default async function Page() {
const users = await db.user.findMany() // DB の秘密鍵は露出しない
return <div>{users.map(...)}</div>
}
```
メリット:
• useEffect、useState が不要
• 競合状態を気にしなくてよい
• loading 状態を扱わなくてよい
• クライアントよりデータ取得がずっとシンプル
いつ fetch を使い、いつデータベースを直接叩くべきか?
• より速い:API 層が 1 段少なく遅延が低い(サーバー→DB は多くの場合 10ms 未満、クライアント→サーバーは 100ms 超も)
• より安全:DB の秘密鍵がクライアントに漏れない
• 向き:DB がサーバーから到達可能なとき
fetch を使う場合:
• 向き:サードパーティ API
• 向き:クロスオリジンが必要なとき
• 向き:既存 API があり、アーキテクチャを変えたくないとき
選び方:
• まず DB 直叩き
• サードパーティ API が必要なときだけ fetch
• Server Component から自分の API を fetch するのは避ける(遠回り)
コード比較:
```tsx
// ❌ アンチパターン:Server Component から自分の API を fetch
const res = await fetch('/api/users')
const users = await res.json()
// ✅ 正解:DB を直接クエリ
const users = await db.user.findMany()
```
async コンポーネントはどう書くか?
```tsx
export default async function Page() {
const data = await fetchData()
return <div>{data}</div>
}
```
並列取得(Promise.all):
```tsx
export default async function Page() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
])
return <div>...</div>
}
```
Suspense でローディングを最適化:
```tsx
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserList />
</Suspense>
)
}
async function UserList() {
const users = await fetchUsers()
return <div>{users.map(...)}</div>
}
```
ポイント:
• Server Components はそのまま async にできる
• Promise.all で並列取得
• Suspense でローディング体験を最適化
Next.js 15 のキャッシュ戦略はどう設定するか?
キャッシュなし(リアルタイム):
```tsx
fetch(url, { cache: 'no-store' })
```
永久キャッシュ(静的データ):
```tsx
fetch(url, { cache: 'force-cache' })
```
定期更新(ISR):
```tsx
fetch(url, { next: { revalidate: 3600 } })
```
選び方:
• リアルタイム → cache: 'no-store'
• 静的データ → cache: 'force-cache'
• 頻繁に更新するがリアルタイム不要 → revalidate
注意:Next.js 15 は「明示的であること」を重視。どのデータをキャッシュするか自分で決める。移行時は破壊的変更に注意。
Server Components のエラー処理はどうするか?
```tsx
const res = await fetch(url)
if (!res.ok) {
throw new Error('Failed to fetch')
}
const data = await res.json()
```
error.tsx でフォールバック:
```tsx
// app/page/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>エラー: {error.message}</h2>
<button onClick={reset}>再試行</button>
</div>
)
}
```
redirect に注意:
```tsx
// ❌ 誤り:redirect を try/catch の中に
try {
if (!user) redirect('/login')
} catch (e) {
// redirect はエラーを投げるため catch される
}
// ✅ 正解:redirect は try/catch の外
if (!user) redirect('/login')
try {
// その他の処理
} catch (e) {
// エラー処理
}
```
ポイント:
• response.ok を確認
• error.tsx でフォールバック
• redirect は try/catch の中に入れない(redirect はエラーを投げる)
4分で読めます · 公開日: 2025年12月19日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js Pages Router から App Router への移行実践ガイド:段階的戦略と回避すべき落とし穴
Next.js Pages Router から App Router へゼロから移行する方法を解説。段階的な移行プラン、7 つのよくある落とし穴の対処法、本番プロジェクトの実体験をもとに、Next.js 14 へ安全にアップグレードする手順を紹介します。
第 7 / 47 記事
次の記事
Next.js App Router 実践:ルートグループとネストレイアウトで大規模プロジェクトのディレクトリ混乱を解決する
ルートグループ、ネストレイアウト、パラレルルート、インターセプトルートの 4 機能で Next.js 大規模プロジェクトのディレクトリ混乱・ルート競合・チーム協業問題を解決し、すぐ使える完全なディレクトリ構造を提供します。
第 9 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
コメント
GitHubアカウントでログインしてコメントできます