言語を切り替える
テーマを切り替える

Next.js Server Components データ取得完全ガイド:fetch、データベースクエリとベストプラクティス

Next.js の App Router で初めてコンポーネントを書いたとき、次のようなコードを目にしました。

async function Page() {
  const data = await fetch('...')
  return <div>{data}</div>
}

これだけ? いきなり awaituseEffectuseState もなく、競合状態の心配もない?

クライアント向け 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

ポイント

  1. コンポーネントを async function として宣言する。
  2. データベースクエリを直接 await できる。
  3. React Hooks(useState, useEffect)は使えない。
  4. デフォルトではサーバーでレンダリングされ、クライアントには生成された 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つの質問に答えてください:

  1. これは Server Component ですか? → YES なら次へ。NO(Client Component)なら質問3へ。
  2. データはどこにありますか?
    • 自分のデータベース → データベース直接クエリ
    • 外部 API → fetch API
  3. 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>
  )
}

設計判断の理由

  1. なぜ DB 直叩き? → Server Component なので API ルートを経由する必要がない
  2. なぜ記事と著者を並列? → 依存がないので並列の方が速い
  3. なぜおすすめは Suspense? → 重要度が低く、遅くてもメインを止めない
  4. なぜ revalidate: 3600? → 記事はあまり変わらない。1 時間キャッシュで DB 負荷を下げる

まとめ

長くなりましたが、核心は次の 4 点です。

  1. Server Component では DB を直接叩くのが基本。Client Component から取る必要があるとき、またはサードパーティ API のときだけ fetch
  2. async/await コンポーネントはシンプル。ただし Promise.all で並列化し、Suspense でローディング体験を整える。
  3. Next.js 15 はデフォルトでキャッシュしない。データに応じて force-cacheno-storerevalidate を選ぶ。
  4. エラーはきちんと処理するresponse.ok を確認し、error.tsx でフォールバック。redirect は try/catch の中に入れない。

Server Components のデータ取得は、クライアントよりずっと楽です。loading 状態、競合、リクエストキャンセルを気にしなくてよい。App Router への移行を迷っているなら、この点だけでも試す価値があります。

次のプロジェクトで積極的に使ってみて、困ったらこの記事に戻ってきてください。

Next.js Server Components データ取得の完全フロー

fetch とデータベースクエリの選び方から async コンポーネント、キャッシュ戦略、エラー処理までの手順

⏱️ 目安時間: 1 時間

  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

    ステップ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

    ステップ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

    ステップ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 はブラウザではなくサーバーで動くからです。

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 コンポーネントはどう書くか?
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 のキャッシュ戦略はどう設定するか?
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 のエラー処理はどうするか?
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 の中に入れない(redirect はエラーを投げる)

4分で読めます · 公開日: 2025年12月19日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます