コンテンツにスキップ
アプリケーションの構築データフェッチデータフェッチとキャッシング

データフェッチとキャッシング

このガイドでは、Next.jsにおけるデータフェッチとキャッシングの基本について、実践的な例とベストプラクティスを交えて説明します。

Next.js でのデータフェッチの最小限の例を以下に示します。

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

この例では、非同期Reactサーバーコンポーネントで`fetch` APIを使用して、基本的なサーバーサイドデータフェッチを実行しています。

リファレンス

`fetch` APIを使用したサーバー上でのデータフェッチ

このコンポーネントは、ブログ投稿のリストをフェッチして表示します。 `fetch` からのレスポンスは自動的にキャッシュされます。

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

このルートの他の場所で動的APIを使用していない場合、`next build` 中に静的ページにプリレンダリングされます。その後、データは差分静的再生成を使用して更新できます。

`fetch` からのレスポンスをキャッシュしたくない場合は、次の操作を実行できます。

let data = await fetch('https://api.vercel.app/blog', { cache: 'no-store' })

ORMまたはデータベースを使用したサーバー上でのデータフェッチ

このコンポーネントはブログ投稿のリストを取得して表示します。データベースからのレスポンスはデフォルトではキャッシュされませんが、追加設定を行うことでキャッシュできます。

app/page.tsx
import { db, posts } from '@/lib/db'
 
export default async function Page() {
  let allPosts = await db.select().from(posts)
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

このルートの他の場所で動的APIを使用していない場合、`next build` 中に静的ページにプリレンダリングされます。その後、データは差分静的再生成を使用して更新できます。

ページのプリレンダリングを防ぐには、ファイルに以下を追加します。

export const dynamic = 'force-dynamic'

ただし、一般的には、cookiesheaders、またはページプロップから受信するsearchParamsの読み取りなどの関数を使用します。これらの関数は、ページを自動的に動的にレンダリングします。この場合、明示的にforce-dynamicを使用する必要はありません

クライアント側でのデータフェッチ

最初にサーバー側でデータを取得することをお勧めします。

ただし、クライアント側でのデータフェッチが理にかなっている場合もあります。このようなシナリオでは、useEffectで手動でfetchを呼び出す(推奨されません)か、コミュニティで人気のあるReactライブラリ(SWRReact Queryなど)を利用してクライアントフェッチを行うことができます。

app/page.tsx
'use client'
 
import { useState, useEffect } from 'react'
 
export function Posts() {
  const [posts, setPosts] = useState(null)
 
  useEffect(() => {
    async function fetchPosts() {
      let res = await fetch('https://api.vercel.app/blog')
      let data = await res.json()
      setPosts(data)
    }
    fetchPosts()
  }, [])
 
  if (!posts) return <div>Loading...</div>
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

ORMまたはデータベースを使用したデータのキャッシュ

unstable_cache APIを使用してレスポンスをキャッシュし、next buildの実行時にページをプリレンダリングできるようにすることができます。

app/page.tsx
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
 
const getPosts = unstable_cache(
  async () => {
    return await db.select().from(posts)
  },
  ['posts'],
  { revalidate: 3600, tags: ['posts'] }
)
 
export default async function Page() {
  const allPosts = await getPosts()
 
  return (
    <ul>
      {allPosts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

この例では、データベースクエリの結果を1時間(3600秒)キャッシュします。また、キャッシュタグpostsを追加します。これは、増分静的再生成を使用して無効化できます。

複数の関数間でのデータの再利用

Next.jsは、generateMetadatagenerateStaticParamsなどのAPIを使用します。これらのAPIでは、pageでフェッチされた同じデータを使用する必要があります。

fetchを使用している場合、リクエストは自動的にメモ化されます。つまり、同じURLを同じオプションで安全に呼び出すことができ、リクエストは1回だけ行われます。

app/page.tsx
import { notFound } from 'next/navigation'
 
interface Post {
  id: string
  title: string
  content: string
}
 
async function getPost(id: string) {
  let res = await fetch(`https://api.vercel.app/blog/${id}`)
  let post: Post = await res.json()
  if (!post) notFound()
  return post
}
 
export async function generateStaticParams() {
  let posts = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json()
  )
 
  return posts.map((post: Post) => ({
    id: post.id,
  }))
}
 
export async function generateMetadata({ params }: { params: { id: string } }) {
  let post = await getPost(params.id)
 
  return {
    title: post.title,
  }
}
 
export default async function Page({ params }: { params: { id: string } }) {
  let post = await getPost(params.id)
 
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

fetchを使用せず、代わりにORMまたはデータベースを直接使用している場合は、データフェッチをReactのcache関数でラップできます。これにより、重複が排除され、クエリは1回だけ実行されます。

import { cache } from 'react'
import { db, posts, eq } from '@/lib/db' // Example with Drizzle ORM
import { notFound } from 'next/navigation'
 
export const getPost = cache(async (id) => {
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, parseInt(id)),
  })
 
  if (!post) notFound()
  return post
})

キャッシュされたデータの再検証

増分静的再生成を使用したキャッシュされたデータの再検証の詳細をご覧ください。

パターン

並列および順次データフェッチ
  • 順次:コンポーネントツリー内のリクエストは互いに依存しています。これは、読み込み時間が長くなる可能性があります。
  • 並列:ルート内のリクエストは積極的に開始され、同時にデータを読み込みます。これにより、データの読み込みにかかる合計時間が短縮されます。

順次データフェッチ

ネストされたコンポーネントがあり、各コンポーネントが独自のデータをフェッチする場合、それらのデータリクエストがメモ化されていないと、データフェッチは順次的に行われます。

一方のフェッチが他方の結果に依存しているため、このパターンが必要な場合があります。たとえば、Playlistsコンポーネントは、Artistコンポーネントがデータのフェッチを完了した後にのみデータのフェッチを開始します。これは、PlaylistsartistIDプロップに依存しているためです。

app/artist/[username]/page.tsx
export default async function Page({
  params: { username },
}: {
  params: { username: string }
}) {
  // Get artist information
  const artist = await getArtist(username)
 
  return (
    <>
      <h1>{artist.name}</h1>
      {/* Show fallback UI while the Playlists component is loading */}
      <Suspense fallback={<div>Loading...</div>}>
        {/* Pass the artist ID to the Playlists component */}
        <Playlists artistID={artist.id} />
      </Suspense>
    </>
  )
}
 
async function Playlists({ artistID }: { artistID: string }) {
  // Use the artist ID to fetch playlists
  const playlists = await getArtistPlaylists(artistID)
 
  return (
    <ul>
      {playlists.map((playlist) => (
        <li key={playlist.id}>{playlist.name}</li>
      ))}
    </ul>
  )
}

Reactが結果をストリーミングしている間に、即時読み込み状態を表示するために、loading.js(ルートセグメント用)またはReact <Suspense>(ネストされたコンポーネント用)を使用できます。

これにより、データリクエストによってルート全体がブロックされるのを防ぎ、ユーザーは準備が整ったページの部分を操作できるようになります。

並列データフェッチ

デフォルトでは、レイアウトとページセグメントは並列にレンダリングされます。これは、リクエストが並列に開始されることを意味します。

ただし、async / awaitの性質上、同じセグメントまたはコンポーネント内で待機されているリクエストは、その下にあるすべてのリクエストをブロックします。

データを並列にフェッチするには、データを使用するコンポーネントの外部でリクエストを定義することにより、リクエストを積極的に開始できます。これにより、両方のリクエストを並列に開始することで時間を節約できますが、両方のPromiseが解決されるまで、ユーザーにはレンダリングされた結果は表示されません。

以下の例では、getArtist関数とgetAlbums関数はPageコンポーネントの外部で定義され、Promise.allを使用してコンポーネント内で開始されます。

app/artist/[username]/page.tsx
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: { username },
}: {
  params: { username: string }
}) {
  const artistData = getArtist(username)
  const albumsData = getAlbums(username)
 
  // Initiate both requests in parallel
  const [artist, albums] = await Promise.all([artistData, albumsData])
 
  return (
    <>
      <h1>{artist.name}</h1>
      <Albums list={albums} />
    </>
  )
}

さらに、Suspense Boundaryを追加して、レンダリング作業を分割し、できるだけ早く結果の一部を表示できます。

データのプリロード
components/Item.tsx
import { getItem } from '@/utils/get-item'
 
export const preload = (id: string) => {
  // void evaluates the given expression and returns 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)
  // ...
}

app/item/[id]/page.tsx
import Item, { preload, checkIsAvailable } from '@/components/Item'
 
export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
  // starting loading item data
  preload(id)
  // perform another asynchronous task
  const isAvailable = await checkIsAvailable()
 
  return isAvailable ? <Item id={id} /> : null
}

知っておくと良いこと:「プリロード」関数は、APIではなくパターンであるため、任意の名前を付けることができます。

プリロードパターンでのReact cacheおよびserver-onlyの使用
utils/get-item.ts
import { cache } from 'react'
import 'server-only'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})

このアプローチでは、データを積極的にフェッチし、レスポンスをキャッシュし、このデータフェッチがサーバー側でのみ発生することを保証できます。

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

知っておくと良いこと

機密データがクライアントに公開されるのを防ぐ taintObjectReferencetaintUniqueValueを使用することをお勧めします。

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

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

次に、汚染したいオブジェクトまたは値をexperimental_taintObjectReferenceまたはexperimental_taintUniqueValue関数に渡します。

app/utils.ts
import { queryDataFromDB } from './api'
import {
  experimental_taintObjectReference,
  experimental_taintUniqueValue,
} from 'react'
 
export async function getUserData() {
  const data = await queryDataFromDB()
  experimental_taintObjectReference(
    'Do not pass the whole user object to the client',
    data
  )
  experimental_taintUniqueValue(
    "Do not pass the user's address to the client",
    data,
    data.address
  )
  return data
}
app/page.tsx
import { getUserData } from './data'
 
export async function Page() {
  const userData = getUserData()
  return (
    <ClientComponent
      user={userData} // this will cause an error because of taintObjectReference
      address={userData.address} // this will cause an error because of taintUniqueValue
    />
  )
}