5.

Next.js のデータ取得|fetch・DB・並列化の基本

編集

この記事の要点

  • サーバーコンポーネントは async 関数にできる。コンポーネントの中で直接 await してデータを取る。
  • データベースへ直接問い合わせてよい。認証情報もクエリもブラウザには渡らない。
  • バージョン 16 では fetch既定でキャッシュされない。取得が終わるまで描画がブロックされる点に注意する。
  • 依存関係のない取得は Promise.all で並列化する。await を並べるだけだと直列になる。
  • 同じリクエスト内で同じ関数を何度も呼ぶなら React.cache で束ねる。props のバケツリレーを避けられる。

App Router の大きな利点のひとつが、データ取得のための専用 API を覚えなくてよいことです。サーバーコンポーネントは async 関数にできるため、必要なコンポーネントの中で直接 await すればよく、Pages Router 時代の getServerSideProps のような橋渡し役は不要になりました。この記事では、その基本形と、実務で効いてくる並列化・重複排除を扱います。

1もっとも基本の形

コンポーネントを async にして await するだけです。

// app/blog/page.tsx
export default async function Page() {
  const res = await fetch('https://api.example.com/blog')
  const posts = await res.json()

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

この処理はサーバー上で走るため、レスポンスの JSON がそのままブラウザに流れることはありません。必要な部分だけを HTML にして返せます。

ここで押さえておきたい仕様が 2 つあります。

  • 同じリクエスト内の同一 fetch は自動的にまとめられる(メモ化)。同じ URL・同じオプションであれば、複数のコンポーネントから呼んでも実際の通信は 1 回です。props で引き回さず、必要な場所で呼んでよい理由がこれです。
  • バージョン 16 では fetch の結果は既定でキャッシュされない。そして取得が終わるまでページの描画がブロックされます。バージョン 13 の頃は既定でキャッシュされていたため、古い記事と挙動が食い違います。

「ブロックされる」という性質への対処は 2 通りあります。結果を使い回してよいなら use cache でキャッシュする(キャッシュの仕組みと use cache)、毎回新鮮な値が要るなら Suspense で包んで先に画面を出す(ストリーミングで体感速度を上げる)。どちらを選ぶかがデータ設計の中心になります。

2データベースを直接触る

サーバーコンポーネントからは、ORM やデータベースクライアントを直接呼べます。接続情報もクエリもクライアントバンドルには含まれません。

// app/blog/page.tsx
import { db, posts } from '@/lib/db'

export default async function Page() {
  const allPosts = await db.select().from(posts)

  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

ただし「クライアントに漏れない」ことと「誰が見てよいか」は別の話です。ログインしているユーザーが本当にそのデータを見てよいのかという認可の確認は、必ずサーバー側の取得処理の中で行ってください。URL を直接叩かれる可能性は常にあります。

3直列と並列

実装で最も差が出るのがここです。await を素直に並べると、前の通信が終わるまで次が始まりません。

// これは直列になる(遅い)
const artist = await getArtist(username)
const albums = await getAlbums(username)

依存関係がないなら、先に呼び出しだけ行って後からまとめて待ちます。fetch は呼んだ時点で通信が始まるため、これで並列になります。

// 呼び出しを先に始めてから、まとめて待つ(速い)
const artistData = getArtist(username)
const albumsData = getAlbums(username)

const [artist, albums] = await Promise.all([artistData, albumsData])
直列(await を並べる) getArtist 400ms getAlbums 400ms 合計 800ms 並列(Promise.all でまとめて待つ) getArtist 400ms getAlbums 400ms 合計 400ms
同じ処理でも書き方だけで所要時間が倍変わる。後続が前の結果を必要としないなら、必ず並列にする。

なお Promise.all は 1 つでも失敗すると全体が失敗します。一部が落ちても残りを表示したい場合は Promise.allSettled を使います。

後続が前の結果に依存する「本当に直列でしか書けない」ケースもあります。その場合は、依存する部分だけを子コンポーネントに切り出して Suspense で包むと、先に取れたデータから順に画面へ出せます。

export default async function Page({ params }) {
  const { username } = await params
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>読み込み中…</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

4同じ取得を束ねる React.cache

fetch は自動でメモ化されますが、データベース問い合わせや自作の関数はそうではありません。同じリクエストの中で複数箇所から呼ぶ場合は React.cache で包みます。

// app/lib/user.ts
import { cache } from 'react'

export const getUser = cache(async () => {
  const res = await fetch('https://api.example.com/user')
  return res.json()
})

これで、レイアウトとページの両方から getUser() を呼んでも実行は 1 回です。メタデータ生成(generateMetadata)とページ本体で同じデータが要るときにも定番の手法になります。

注意点として、React.cache の効果はそのリクエストの中だけです。リクエストをまたいで結果を使い回したい場合は、キャッシュの仕組みのほうを使います。

5クライアント側で取得する場合

ブラウザ側でデータを取る必要がある場面もあります。選択肢は主に 2 つです。

手段特徴向いている場面
React の use APIサーバーで開始した Promise をそのまま props で渡し、クライアントで解決する初回表示をストリーミングしたい
SWR / TanStack Query再取得・キャッシュ・失効管理を持つ操作に応じて何度も取り直す画面

use を使う場合、サーバー側では Promise を await せずに渡すのがポイントです。

// app/blog/page.tsx(サーバー)
export default function Page() {
  const posts = getPosts()          // await しない

  return (
    <Suspense fallback={<div>読み込み中…</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}
// app/ui/posts.tsx(クライアント)
'use client'
import { use } from 'react'

export default function Posts({ posts }) {
  const allPosts = use(posts)
  return <ul>{allPosts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}

6つまずきやすいところ

  • 全体が遅い:どこかで直列になっていないか確認します。レイアウトとページはもともと並列に描画されますが、1 つのコンポーネント内の await の並びは直列です。
  • キャッシュされていると思い込む:バージョン 16 の fetch は既定でキャッシュしません。「一度取ったから速いはず」という前提でコードを書くと、負荷試験で崩れます。
  • props のバケツリレーが深いfetch のメモ化や React.cache を使えば、必要なコンポーネントで直接呼ぶ形に整理できます。
  • クライアントから直接 DB を触ろうとする:できません。取得はサーバー、更新は Server FunctionsRoute Handlers を経由します。

取得の書き方が分かったら、次はその待ち時間をどう見せるかです。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点