データ取得パターン

ReactとNext.jsでデータを取得する際に推奨されるパターンとベストプラクティスがいくつかあります。このページでは、最も一般的なパターンとその使用方法について説明します。

サーバーでのデータ取得

可能な限り、サーバーでデータを取得することを推奨します。これにより、以下の利点があります:

  • バックエンドのデータリソース(データベースなど)に直接アクセス可能
  • アクセストークンやAPIキーなどの機密情報をクライアントに公開せず、アプリケーションのセキュリティを向上
  • データ取得とレンダリングを同じ環境で実行可能。これによりクライアントとサーバー間の往復通信と、クライアント側のメインスレッドの作業を削減
  • クライアント側で個別にリクエストする代わりに、単一の往復で複数のデータ取得を実行可能
  • クライアント-サーバー間のウォーターフォールを削減
  • 地域によっては、データソースに近い場所でデータ取得が可能になり、レイテンシが減少しパフォーマンスが向上

サーバーでのデータ取得には、サーバーコンポーネント、Route Handlers(ルートハンドラ)Server Actions(サーバーアクション)を使用できます。

必要な場所でのデータ取得

ツリー内の複数のコンポーネントで同じデータ(現在のユーザーなど)を使用する必要がある場合、データをグローバルに取得したり、コンポーネント間でプロパティを転送する必要はありません。代わりに、データが必要なコンポーネント内でfetchまたはReactのcacheを使用できます。同じデータに対する複数のリクエストによるパフォーマンスへの影響を心配する必要はありません。

これはfetchリクエストが自動的にメモ化されるため可能です。リクエストメモ化について詳しく学べます

補足:これはレイアウトにも適用されます。親レイアウトとその子コンポーネント間でデータを渡すことはできません。

ストリーミング

ストリーミングとSuspense(サスペンス)は、UIのレンダリング単位をクライアントに段階的にレンダリングし、インクリメンタルにストリーミングできるReactの機能です。

サーバーコンポーネントとネストされたレイアウトを使用すると、データを特に必要としないページの一部を即座にレンダリングし、データを取得中のページの部分に対してローディング状態を表示できます。これにより、ユーザーはページ全体が読み込まれるのを待たずに、ページとのインタラクションを開始できます。

ストリーミングによるサーバーレンダリング

ストリーミングとSuspenseについて詳しくは、ローディングUIストリーミングとSuspenseのページを参照してください。

並列および逐次的なデータ取得

Reactコンポーネント内でデータを取得する場合、2つのデータ取得パターンに注意する必要があります:並列と逐次的です。

逐次的および並列なデータ取得
  • 逐次的なデータ取得では、ルート内のリクエストが互いに依存しているため、ウォーターフォールが発生します。あるフェッチが他の結果に依存している場合や、次のフェッチ前に条件を満たしたい場合など、このパターンが望ましい場合があります。ただし、この動作は意図せずに発生し、読み込み時間が長くなる可能性もあります。
  • 並列データ取得では、ルート内のリクエストが積極的に開始され、データが同時に読み込まれます。これにより、クライアント-サーバー間のウォーターフォールとデータ読み込みの総時間が削減されます。

逐次的なデータ取得

ネストされたコンポーネントがあり、各コンポーネントが独自のデータを取得する場合、それらのデータリクエストが異なると、データ取得は逐次的に行われます(同じデータに対するリクエストは自動的にメモ化されるため、この限りではありません)。

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

// ...

async function Playlists({ artistID }: { artistID: string }) {
  // プレイリストを待機
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // アーティストを待機
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
// ...

async function Playlists({ artistID }) {
  // プレイリストを待機
  const playlists = await getArtistPlaylists(artistID)

  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

export default async function Page({ params: { username } }) {
  // アーティストを待機
  const artist = await getArtist(username)

  return (
    <>
      <h1>{artist.name}</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}

このような場合、loading.js(ルートセグメント用)またはReactの<Suspense>(ネストされたコンポーネント用)を使用して、Reactが結果をストリーミングしている間に即時のローディング状態を表示できます。

これにより、データ取得によってルート全体がブロックされるのを防ぎ、ユーザーはブロックされていないページの部分とインタラクションできます。

データリクエストのブロック:

ウォーターフォールを防ぐ別の方法は、アプリケーションのルートでグローバルにデータを取得することですが、これによりデータの読み込みが完了するまで、その下のすべてのルートセグメントのレンダリングがブロックされます。これは「すべてか無か」のデータ取得と表現できます。ページまたはアプリケーションのデータ全体を取得するか、まったく取得しないかのどちらかです。

awaitを使用したすべてのフェッチリクエストは、<Suspense>境界でラップされるかloading.jsが使用されない限り、その下のツリー全体のレンダリングとデータ取得をブロックします。別の方法として、並列データ取得またはプリロードパターンを使用できます。

並列データ取得

データを並列に取得するには、データを使用するコンポーネントの外部でリクエストを定義し、コンポーネント内から呼び出すことで、リクエストを積極的に開始できます。これにより両方のリクエストを並列に開始することで時間を節約できますが、両方のPromiseが解決されるまで、ユーザーはレンダリング結果を確認できません。

以下の例では、getArtistgetArtistAlbums関数がPageコンポーネントの外部で定義され、コンポーネント内で呼び出され、両方のPromiseが解決されるのを待ちます:

import Albums from './albums'

async function getArtist(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getArtistAlbums(username: string) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // 両方のリクエストを並列に開始
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Promiseが解決されるのを待機
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}
import Albums from './albums'

async function getArtist(username) {
  const res = await fetch(`https://api.example.com/artist/${username}`)
  return res.json()
}

async function getArtistAlbums(username) {
  const res = await fetch(`https://api.example.com/artist/${username}/albums`)
  return res.json()
}

export default async function Page({ params: { username } }) {
  // 両方のリクエストを並列に開始
  const artistData = getArtist(username)
  const albumsData = getArtistAlbums(username)

  // Promiseが解決されるのを待機
  const [artist, albums] = await Promise.all([artistData, albumsData])

  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums}></Albums>
    </>
  )
}

ユーザーエクスペリエンスを向上させるために、Suspense Boundary(サスペンス境界)を追加して、レンダリング作業を分割し、可能な限り早く結果の一部を表示できます。

データのプリロード

ウォーターフォールを防ぐもう1つの方法は、プリロードパターンを使用することです。並列データ取得をさらに最適化するために、オプションでpreload関数を作成できます。このアプローチでは、Promiseをプロパティとして渡す必要はありません。preload関数はAPIではなくパターンであるため、任意の名前を付けることができます。

import { getItem } from '@/utils/get-item'

export const preload = (id: string) => {
  // voidは与えられた式を評価しundefinedを返します
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }: { id: string }) {
  const result = await getItem(id)
  // ...
}
import { getItem } from '@/utils/get-item'

export const preload = (id) => {
  // voidは与えられた式を評価しundefinedを返します
  // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void
  void getItem(id)
}
export default async function Item({ id }) {
  const result = await getItem(id)
  // ...
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // アイテムデータの読み込みを開始
  preload(id)
  // 別の非同期タスクを実行
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}
import Item, { preload, checkIsAvailable } from '@/components/Item'

export default async function Page({ params: { id } }) {
  // アイテムデータの読み込みを開始
  preload(id)
  // 別の非同期タスクを実行
  const isAvailable = await checkIsAvailable()

  return isAvailable ? <Item id={id} /> : null
}

Reactのcacheserver-only、およびプリロードパターンの使用

cache関数、preloadパターン、およびserver-onlyパッケージを組み合わせて、アプリ全体で使用できるデータ取得ユーティリティを作成できます。

import { cache } from 'react'
import 'server-only'

export const preload = (id: string) => {
  void getItem(id)
}

export const getItem = cache(async (id: string) => {
  // ...
})
import { cache } from 'react'
import 'server-only'

export const preload = (id) => {
  void getItem(id)
}

export const getItem = cache(async (id) => {
  // ...
})

このアプローチにより、データを積極的に取得し、レスポンスをキャッシュし、このデータ取得がサーバーでのみ行われることを保証できます。

utils/get-itemのエクスポートは、レイアウト、ページ、または他のコンポーネントで使用でき、アイテムのデータがいつ取得されるかを制御できます。

補足:

  • サーバーデータ取得関数がクライアントで使用されないように、server-onlyパッケージの使用を推奨します。