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

データ取得とキャッシュ

このガイドでは、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を使用して更新できます。

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

export const dynamic = 'force-dynamic'

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

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ライブラリ(例: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を追加しており、後でIncremental Static Regenerationでインバリデートすることができます。

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

Next.jsは、generateMetadatagenerateStaticParamsといったAPIを使用します。これらのAPIでは、pageで取得したデータを使う必要があります。

もしfetchを使用している場合、リクエストにcache: 'force-cache'を追加するとリクエストがメモ化されます。これにより、同じオプションで同じURLを安全に呼び出すことができ、リクエストは1回のみ行われます。

Good to know:

  • 以前のバージョンのNext.jsでは、fetchを使用するとデフォルトのcache値はforce-cacheでした。バージョン15では、デフォルトがcache: no-storeに変更されました。
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}`, {
cache: 'force-cache',
})
let post: Post = await res.json()
if (!post) notFound()
return post
}

export async function generateStaticParams() {
let posts = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
}).then((res) => res.json())

return posts.map((post: Post) => ({
id: post.id,
}))
}

export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}) {
let post = await getPost(params.id)

return {
title: post.title,
}
}

export default async function Page({
params,
}: {
params: Promise<{ 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つのデータ取得パターンを意識する必要があります。

順次および並列データ取得順次および並列データ取得
  • 逐次: コンポーネントtree内でリクエストが互いに依存している。この結果、読み込み時間が長くなることがあります
  • 並列: route内のリクエストが積極的に開始され、データは同時に読み込まれます。これにより、データの読み込みにかかる全体の時間が短縮されます

逐次データ取得

ネストされたコンポーネントがあり、各コンポーネントが独自にデータを取得する場合、これらのデータリクエストがメモ化されていない場合、データ取得は逐次的に行われます。

このパターンが望ましい場合もあります。たとえば、PlaylistsコンポーネントはArtistコンポーネントがデータ取得を完了してからデータを取得し始めます。PlaylistsartistID propに依存しているためです:

app/artist/[username]/page.tsx
export default async function Page({
params: { username },
}: {
params: Promise<{ username: string }>
}) {
// アーティスト情報の取得
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>
)
}

loading.js(routeセグメント用)またはReact <Suspense>(ネストされたコンポーネント用)を使用して、即時の読み込み状態を表示することができます。

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

並列データ取得

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

しかし、async/awaitの特性上、同じセグメントまたはコンポーネント内で待たれるリクエストはそれ以下のリクエストをブロックします。

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

下の例では、getArtistgetAlbums関数が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: Promise<{ 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を追加することで、レンダリング作業を分割し可能な限り早く部分的な結果を表示することができます。

データの事前ロード

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

なお、preload関数はcheckIsAvailable()の実行をブロックしません。

components/Item.tsx
import { getItem } from '@/utils/get-item'

export const preload = (id: string) => {
// voidは指定された式を評価して未定義を返します。
// 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: Promise<{ id: string }>
}) {
// アイテムデータの読み込みを開始
preload(id)
// 他の非同期タスクを実行
const isAvailable = await checkIsAvailable()

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

Good to know: "preload"関数はAPIではなくパターンであるため、任意の名前を持つことができます。

Reactのcacheserver-onlyを使用したPreloadパターン

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エクスポートは、レイアウト、ページ、または他のコンポーネントによって使用され、アイテムのデータ取得のタイミングを制御することができます。

Good to know:

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

クライアントへの機密データの露出を防ぐ

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

アプリケーションで汚染を有効にするには、Next.js Configの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} // taintObjectReferenceによるエラーが発生します
address={userData.address} // taintUniqueValueによるエラーが発生します
/>
)
}