データ取得とストリーミングの方法

このページでは、サーバーコンポーネントとクライアントコンポーネントでデータを取得する方法と、データに依存するコンポーネントをストリーミングする方法について説明します。

データの取得

サーバーコンポーネント

サーバーコンポーネントでは以下の方法でデータを取得できます:

  1. fetch APIを使用する
  2. ORMまたはデータベースを使用する

fetch APIを使用する

fetch APIでデータを取得するには、コンポーネントを非同期関数に変更し、fetch呼び出しをawaitします。例:

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またはデータベースを使用する

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

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>
  )
}

クライアントコンポーネント

クライアントコンポーネントでデータを取得するには2つの方法があります:

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

useフックでデータをストリーミングする

Reactのuseフックを使用して、サーバーからクライアントへデータをストリーミングできます。まずサーバーコンポーネントでデータを取得し、Promiseをクライアントコンポーネントにpropsとして渡します:

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>
  )
}

次に、クライアントコンポーネントでuseフックを使用してPromiseを読み取ります:

'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>境界でラップされています。これはPromiseが解決される間、フォールバックが表示されることを意味します。ストリーミングについて詳しく学んでください。

コミュニティライブラリを使用する

SWRReact Queryなどのコミュニティライブラリを使用してクライアントコンポーネントでデータを取得できます。これらのライブラリには、キャッシュ、ストリーミングなどの独自のセマンティクスがあります。例えば、SWRを使用する場合:

'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>
  )
}

React.cacheでリクエストを重複排除

重複排除とは、レンダリングパス中に同じリソースに対する重複リクエストを防ぐプロセスです。これにより、異なるコンポーネントで同じデータを取得しながら、データソースへの複数のネットワークリクエストを防ぐことができます。

fetchを使用している場合、cache: 'force-cache'を追加することでリクエストを重複排除できます。これは同じURLとオプションで安全に呼び出し、1つのリクエストのみが行われることを意味します。

fetchを使用せず、代わりにORMやデータベースを直接使用している場合は、データ取得をReact cache関数でラップできます。

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)),
  })
})

ストリーミング

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

サーバーコンポーネントでasync/awaitを使用すると、Next.jsは動的レンダリングを選択します。これはデータがサーバーで取得され、ユーザーリクエストごとにレンダリングされることを意味します。遅いデータリクエストがある場合、ルート全体のレンダリングがブロックされます。

初期読み込み時間とユーザーエクスペリエンスを向上させるために、ストリーミングを使用してページのHTMLを小さなチャンクに分割し、それらのチャンクをサーバーからクライアントに段階的に送信できます。

サーバーレンダリングとストリーミングの仕組み

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

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

loading.jsを使用する

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

loading.jsファイルを含むブログフォルダ構造
export default function Loading() {
  // ローディングUIをここで定義
  return <div>Loading...</div>
}

ナビゲーション時に、ユーザーはレイアウトとローディング状態をすぐに確認でき、レンダリングが完了すると新しいコンテンツが自動的に切り替わります。

ローディングUI

内部的には、loading.jslayout.js内にネストされ、page.jsファイルとその下の子を自動的に<Suspense>境界でラップします。

loading.jsの概要

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

<Suspense>を使用する

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

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を使用してコンポーネントのローディング状態をプレビューおよび検査できます。

シーケンシャルなデータ取得

シーケンシャルなデータ取得は、ツリー内のネストされたコンポーネントがそれぞれ独自のデータを取得し、リクエストが重複排除されない場合に発生し、応答時間が長くなります。

シーケンシャルと並列のデータ取得

一方の取得が他方の結果に依存するため、このパターンが必要な場合があります。

例えば、<Playlists>コンポーネントは<Artist>コンポーネントがデータの取得を終了した後にのみデータの取得を開始します。これは<Playlists>artistIDプロップに依存しているためです:

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>
  )
}

ユーザーエクスペリエンスを向上させるには、データが取得されている間にfallbackを表示するためにReact <Suspense>を使用してください。これによりストリーミングが有効になり、シーケンシャルなデータリクエストによってルート全体がブロックされるのを防ぎます。

並列データ取得 (Parallel data fetching)

並列データ取得とは、ルート内のデータリクエストが積極的に開始され、同時に実行されることを指します。

デフォルトでは、レイアウトとページは並列にレンダリングされます。そのため、各セグメントは可能な限り早くデータの取得を開始します。

ただし、任意の コンポーネント内では、複数の async/await リクエストが連続して配置されている場合、それらは順次実行されます。例えば、以下の例では getAlbumsgetArtist が解決されるまでブロックされます:

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>
}

データを使用するコンポーネントの外側でリクエストを定義し、Promise.all を使用して一緒に解決することで、並列にリクエストを開始できます:

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 メソッドを使用できます。

データのプリロード (Preloading data)

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

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

import { getItem } 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 パッケージ を使用して、再利用可能なユーティリティ関数を作成できます。このアプローチでは、データ取得関数をキャッシュし、サーバー上でのみ実行されるようにすることができます。

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) => {
  // ...
})