Menu

データの取得

このページでは、Server Components と Client Components でデータを取得する方法、およびストリーミングによってデータに依存するコンポーネントを配信する方法について説明します。

データの取得

Server Components

Server Components でデータを取得するには、以下を使用できます。

  1. fetch API
  2. ORM またはデータベース

fetch API の使用

fetch API でデータを取得するには、コンポーネントを非同期関数に変更し、fetch 呼び出しを await します。例えば以下のようになります。

app/blog/page.tsx
TypeScript
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

補足:

  • fetch レスポンスはデフォルトではキャッシュされません。ただし、Next.js はルートを事前レンダリングし、出力がキャッシュされてパフォーマンスが向上します。動的レンダリングを使用したい場合は、{ cache: 'no-store' } オプションを使用してください。fetch API リファレンスを参照してください。
  • 開発時には、fetch 呼び出しをログして、より適切に可視化およびデバッグできます。logging API リファレンスを参照してください。

ORM またはデータベースの使用

Server Components はサーバー上でレンダリングされるため、ORM またはデータベース クライアントを使用してデータベース クエリを安全に実行できます。コンポーネントを非同期関数に変更し、呼び出しを await します。

app/blog/page.tsx
TypeScript
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>
  )
}

Client Components

Client Components でデータを取得する方法は 2 つあります。

  1. React の use フック
  2. SWRReact Query などのコミュニティ ライブラリ

use フックを使用したデータのストリーミング

React の use フックを使用して、サーバーからクライアントへストリーミングにより、データを取得できます。まず Server Component でデータを取得し、プロミスを Client Component にプロップとして渡します。

app/blog/page.tsx
TypeScript
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
 
export default function Page() {
  // データ取得関数を await しないでください
  const posts = getPosts()
 
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Posts posts={posts} />
    </Suspense>
  )
}

次に、Client Component で use フックを使用してプロミスを読み取ります。

app/ui/posts.tsx
TypeScript
'use client'
import { use } from 'react'
 
export default function Posts({
  posts,
}: {
  posts: Promise<{ id: string; title: string }[]>
}) {
  const allPosts = use(posts)
 
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

上記の例では、<Posts> コンポーネントは <Suspense> 境界にラップされています。これはプロミスが解決されている間、フォールバックが表示されることを意味します。ストリーミングについて詳しく学習してください。

コミュニティ ライブラリ

SWRReact Query などのコミュニティ ライブラリを使用して、Client Components でデータを取得できます。これらのライブラリには、キャッシング、ストリーミング、その他の機能のための独自のセマンティクスがあります。例えば、SWR では以下のようになります。

app/blog/page.tsx
TypeScript
'use client'
import useSWR from 'swr'
 
const fetcher = (url) => fetch(url).then((r) => r.json())
 
export default function BlogPage() {
  const { data, error, isLoading } = useSWR(
    'https://api.vercel.app/blog',
    fetcher
  )
 
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
 
  return (
    <ul>
      {data.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

リクエストの重複排除とデータのキャッシング

fetch リクエストを重複排除する方法の一つは、リクエスト メモ化を使用することです。このメカニズムを使用すると、単一のレンダリング パス内で同じ URL とオプションで GET または HEAD を使用する fetch 呼び出しが 1 つのリクエストに統合されます。これは自動的に発生し、fetch に Abort シグナルを渡すことでオプトアウトできます。

リクエスト メモ化はリクエストのライフタイムにスコープされます。

また、Next.js のデータ キャッシュを使用して fetch リクエストを重複排除することもできます。例えば、fetch オプションで cache: 'force-cache' を設定します。

データ キャッシュ を使用すると、現在のレンダリング パスと受信リクエストの間でデータを共有できます。

fetch を使用しておらず、代わりに ORM またはデータベースを直接使用している場合は、React cache 関数でデータ アクセスをラップできます。

app/lib/data.ts
TypeScript
import { cache } from 'react'
import { db, posts, eq } from '@/lib/db'
 
export const getPost = cache(async (id: string) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
})

ストリーミング

警告: 以下のコンテンツは、アプリケーションで cacheComponents 設定オプションが有効になっていることを前提としています。このフラグは Next.js 15 canary で導入されました。

Server Components でデータを取得する場合、データはサーバー上でフェッチされ、各リクエストに対してサーバー上でレンダリングされます。遅いデータ リクエストがある場合、すべてのデータがフェッチされるまで、ルート全体がレンダリングから遮断されます。

初期ロード時間とユーザー エクスペリエンスを向上させるには、ストリーミングを使用してページの HTML をより小さなチャンクに分割し、これらのチャンクをサーバーからクライアントへ段階的に送信します。

ストリーミングを使用したサーバー レンダリングの仕組み

アプリケーションにストリーミングを実装する方法は 2 つあります。

  1. ページを loading.js ファイルでラップする
  2. コンポーネントを <Suspense> でラップする

loading.js の使用

ページと同じフォルダに loading.js ファイルを作成して、データがフェッチされている間、ページ全体をストリーミングできます。例えば、app/blog/page.js をストリーミングするには、app/blog フォルダ内にファイルを追加します。

loading.js ファイルのあるブログ フォルダ構造
app/blog/loading.tsx
TypeScript
export default function Loading() {
  // ここにロード中の UI を定義します
  return <div>Loading...</div>
}

ナビゲーション時、ユーザーはレイアウトとロード中の状態を即座に確認します。ページのレンダリングが完了すると、新しいコンテンツが自動的にスワップインされます。

ロード中の UI

舞台裏では、loading.jslayout.js 内にネストされ、page.js ファイルおよび下の任意の子要素を自動的に <Suspense> 境界でラップします。

loading.js の概要

このアプローチはルート セグメント(レイアウトとページ)に適していますが、より細かなストリーミングの場合は、<Suspense> を使用できます。

<Suspense> の使用

<Suspense> を使用すると、ページのどの部分をストリーミングするかについて、より細かく制御できます。例えば、<Suspense> 境界の外にあるページ コンテンツをすぐに表示し、境界内のブログ記事リストをストリーミングできます。

app/blog/page.tsx
TypeScript
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
 
export default function BlogPage() {
  return (
    <div>
      {/* このコンテンツはクライアントに即座に送信されます */}
      <header>
        <h1>ブログへようこそ</h1>
        <p>以下の最新の記事をお読みください。</p>
      </header>
      <main>
        {/* <Suspense> 境界にラップされたコンテンツはストリーミングされます */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

意味のあるロード中の状態の作成

ロード中の状態は、ナビゲーション後にユーザーに即座に表示されるフォールバック UI です。ユーザー エクスペリエンスを最適にするために、意味のあるロード中の状態を設計し、ユーザーがアプリが応答していることを理解するのを支援することをお勧めします。例えば、スケルトンやスピナー、または将来の画面の小さくても意味のある部分(カバー画像、タイトルなど)を使用できます。

開発時には、React Devtools を使用してコンポーネントのロード中の状態をプレビューして検査できます。

順序付けされたデータ取得

順序付けされたデータ取得は、ツリー内のネストされたコンポーネントがそれぞれ独自のデータをフェッチし、リクエストが重複排除されないため、応答時間が長くなる場合に発生します。

順序付けされたデータ取得と並行データ取得

1 つのフェッチが別のフェッチの結果に依存する場合など、このパターンが必要な場合があります。

例えば、<Playlists> コンポーネントは artistID プロップに依存しているため、<Artist> コンポーネントがデータのフェッチを完了するまで、データのフェッチを開始しません。

app/artist/[username]/page.tsx
TypeScript
export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
  // アーティスト情報を取得
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      {/* Playlists コンポーネントがロード中の間、フォールバック UI を表示 */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* アーティスト ID を Playlists コンポーネントに渡す */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
 
async function Playlists({ artistID }: { artistID: string }) {
  // アーティスト ID を使用してプレイリストを取得
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

ユーザー エクスペリエンスを向上させるには、React <Suspense> を使用して、データがフェッチされている間、fallback を表示する必要があります。これにより、ストリーミングが有効になり、ルート全体が順序付けされたデータ リクエストでブロックされるのを防ぎます。

並行データ取得

並行データ取得は、ルート内のデータ リクエストが積極的に開始され、同時に開始される場合に発生します。

デフォルトでは、レイアウトとページは並行してレンダリングされます。したがって、各セグメントはできるだけ早くデータのフェッチを開始します。

ただし、任意のコンポーネント内では、複数の async/await リクエストは相互の後に配置されている場合は、順序付けされたままです。例えば、getAlbumsgetArtist が解決されるまでブロックされます。

app/artist/[username]/page.tsx
TypeScript
import { getArtist, getAlbums } from '@/app/lib/data'
 
export default async function Page({ params }) {
  // これらのリクエストは順序付けされます
  const { username } = await params
  const artist = await getArtist(username)
  const albums = await getAlbums(username)
  return <div>{artist.name}</div>
}

fetch を呼び出すことで複数のリクエストを開始し、Promise.all で await します。fetch が呼び出されると、リクエストが即座に開始されます。

app/artist/[username]/page.tsx
TypeScript
import Albums from './albums'
 
async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}
 
async function getAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}
 
export default async function Page({
  params,
}: {
  params: Promise<{ username: string }>
}) {
  const { username } = await params
 
  // リクエストを開始
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)
 
  const [artist, albums] = await Promise.all([artistData, albumsData])
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  )
}

補足: Promise.all を使用している場合、1 つのリクエストが失敗すると、操作全体が失敗します。これを処理するには、代わりに Promise.allSettled メソッドを使用できます。

データの事前読み込み

ブロッキング リクエストの上で積極的に呼び出す、ユーティリティ関数を作成することで、データを事前読み込みできます。<Item>checkIsAvailable() 関数に基づいて条件付きでレンダリングされます。

checkIsAvailable() の前に preload() を呼び出して、<Item/> のデータ依存関係を積極的に開始できます。<Item/> がレンダリングされるまでに、そのデータはすでにフェッチされています。

app/item/[id]/page.tsx
TypeScript
import { getItem, checkIsAvailable } from '@/lib/data'
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  // アイテム データの読み込みを開始
  preload(id)
  // 別の非同期タスクを実行
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}
 
export const preload = (id: string) => {
  // void は与えられた式を評価し、undefined を返します
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}

さらに、React の cache 関数server-only パッケージを使用して、再利用可能なユーティリティ関数を作成できます。このアプローチにより、データ取得関数をキャッシュしながら、サーバー上でのみ実行されるようにできます。

utils/get-item.ts
TypeScript
import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})