メインコンテンツまでスキップ

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

Examples

このガイドでは、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 server component内で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>
)
}

このルートの他の箇所でDynamic APIsを使用していない場合、next build中に静的ページとして事前レンダリングされます。このデータはIncremental Static Regenerationを使用して更新できます。

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

このルートの他の箇所でDynamic APIsを使用していない場合、next build中に静的ページとして事前レンダリングされます。このデータはIncremental Static Regenerationを使用して更新できます。

ページの事前レンダリングを防ぐには、次のものをファイルに追加できます:

export const dynamic = 'force-dynamic'

しかし、通常は、cookiesheaders、またはページpropsからのsearchParamsの読み取りのような関数を使用し、自動的にページが動的にレンダリングされるようにします。この場合、明示的にforce-dynamicを使用する必要はありません。

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

まず、サーバーサイドでのデータフェッチを試みることをお勧めします。

ただし、クライアントサイドでのデータフェッチが意味を持つケースもあります。こういったシナリオでは、useEffect内で手動でfetchを呼び出すか(推奨されません)、コミュニティで人気のあるReactライブラリ(例: SWR または React 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を追加し、これによりIncremental Static Regenerationで無効化できます。

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

Next.jsでは、API generateMetadatagenerateStaticParamsを使用して、pageで取得したのと同じデータを使う必要があります。

fetchを使用している場合、リクエストは自動的にrequest memoizationされます。これは、同じ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' // 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
})

キャッシュされたデータを再検証する

キャッシュされたデータを再検証する方法について、Incremental Static Regenerationで詳しく知ることができます。

パターン

並行データフェッチと逐次データフェッチ

コンポーネント内でデータをフェッチする際には、2つのデータフェッチパターン:並行と逐次に注意する必要があります。

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

逐次データフェッチ

入れ子のコンポーネントがあり、各コンポーネントが独自のデータをフェッチする場合、これらのデータリクエストがrequest memoizationされていない場合、データフェッチは逐次的に行われます。

1つのフェッチが他のフェッチの結果に依存するため、このパターンを望むケースがあるかもしれません。例えば、PlaylistsコンポーネントはartistID propに依存しているため、Artistコンポーネントがデータのフェッチを終えた後にのみデータのフェッチを開始します:

app/artist/[username]/page.tsx
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// アーティスト情報を取得
const artist = await getArtist(username)

return (
<>
<h1>{artist.name}</h1>
{/* Playlistsコンポーネントが読み込まれている間、フォールバックUIを表示 */}
<Suspense fallback={<div>Loading...</div>}>
{/* PlaylistsコンポーネントにアーティストIDを渡す */}
<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>
)
}

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

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

並行データフェッチ

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

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

データを並行してフェッチするには、データを使用するコンポーネントの外側でリクエストを積極的に開始してください。これにより、両方のリクエストが並行して開始されるため時間が節約されますが、両方のプロミスが解決されるまでユーザーはレンダリングされた結果を見られません。

以下の例では、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)

// 両方のリクエストを並行して開始
const [artist, albums] = await Promise.all([artistData, albumsData])

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

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

データの事前読み込み

ウォーターフォールを防ぐ別の方法は、ユーティリティ関数を作成し、ブロックするリクエストの上で積極的に呼び出すことでpreloadパターンを使用することです。例えば、checkIsAvailable()<Item/>のレンダリングをブロックするため、preload()を呼び出して<Item/>データ依存関係を積極的に開始できます。 <Item/>がレンダリングされた時点では、データはすでにフェッチされています。

preload関数はcheckIsAvailable()の実行をブロックしないことに注意してください。

components/Item.tsx
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)
// ...
}
app/item/[id]/page.tsx
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
}

Good to know: "preload"関数はパターンであり、APIではないため、名前には何でも使用できます。

PreloadパターンでReact cacheserver-onlyを使用する

cache関数、preloadパターン、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のエクスポートは、Layouts、Pages、その他のコンポーネントによって使用され、アイテムのデータが取得されるタイミングを制御できます。

Good to know:

  • サーバーデータフェッチ関数がクライアントでは決して使用されないことを確認するためにserver-onlyパッケージの使用をお勧めします。

クライアントに機密データが露出しないようにする

Reactのtaint APIs、taintObjectReferenceおよびtaintUniqueValueを使用して、オブジェクト全体のインスタンスや機密値がクライアントに渡されることがないようにすることをお勧めします。

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

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

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

app/utils.ts
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
}
app/page.tsx
import { getUserData } from './data'

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