パターンとベストプラクティス

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

サーバーでのデータ取得

可能な限り、サーバーコンポーネントを使用してサーバー上でデータを取得することを推奨します。これにより、以下の利点があります:

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

その後、サーバーアクションを使用してデータを変更または更新できます。

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

ツリー内の複数のコンポーネントで同じデータ(現在のユーザーなど)を使用する必要がある場合、データをグローバルに取得したり、コンポーネント間でプロパティを転送したりする必要はありません。代わりに、データを必要とするコンポーネント内で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が使用されていない限り、その下にあるツリー全体のレンダリングとデータ取得をブロックします。もう1つの選択肢は、並列データ取得またはプリロードパターンを使用することです。

並列データ取得

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

以下の例では、getArtist関数とgetArtistAlbums関数がPageコンポーネントの外部で定義され、コンポーネント内で呼び出され、両方のプロミスの解決を待機します:

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)

  // プロミスの解決を待機
  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)

  // プロミスの解決を待機
  const [artist, albums] = await Promise.all([artistData, albumsData])

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

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

データのプリロード

ウォーターフォールを防ぐもう1つの方法は、プリロードパターンを使用することです。オプションでpreload関数を作成し、並列データ取得をさらに最適化できます。このアプローチでは、プロミスをプロパティとして渡す必要はありません。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パッケージの使用を推奨します。

機密データがクライアントに公開されるのを防ぐ

Reactのtaint API、taintObjectReferenceおよびtaintUniqueValueを使用して、オブジェクトインスタンス全体または機密値がクライアントに渡されるのを防ぐことを推奨します。

アプリケーションでtaintを有効にするには、Next.js Configのexperimental.taintオプションをtrueに設定します:

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
}

次に、taintをかけたいオブジェクトまたは値をexperimental_taintObjectReferenceまたはexperimental_taintUniqueValue関数に渡します:

import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'ユーザーオブジェクト全体をクライアントに渡さないでください',
    data
  )
  experimental_taintUniqueValue(
    "ユーザーの住所をクライアントに渡さないでください",
    data,
    data.address
  )
  return data
}
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'

export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'ユーザーオブジェクト全体をクライアントに渡さないでください',
    data
  )
  experimental_taintUniqueValue(
    "ユーザーの住所をクライアントに渡さないでください",
    data,
    data.address
  )
  return data
}
import { getUserData } from './data'

export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // taintObjectReferenceによりエラーが発生します
      address={userData.address} // taintUniqueValueによりエラーが発生します
    />
  )
}
import { getUserData } from './data'

export async function Page() {
  const userData = await getUserData()
  return (
    <ClientComponent
      user={userData} // taintObjectReferenceによりエラーが発生します
      address={userData.address} // taintUniqueValueによりエラーが発生します
    />
  )
}

セキュリティとサーバーアクションについて詳しく学ぶ。