データ取得とキャッシング
このガイドでは、Next.jsにおけるデータ取得とキャッシングの基本を実用的な例とベストプラクティスを交えて説明します。
以下はNext.jsでのデータ取得の最小限の例です:
- TypeScript
- JavaScript
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
この例は、非同期のReact server componentでfetch
APIを使用した基本的なサーバーサイドのデータ取得を示しています。
参考資料
fetch
- React
cache
- Next.js
unstable_cache
例
fetch
APIを使用したサーバーでのデータ取得
このコンポーネントはブログ投稿のリストを取得して表示します。fetch
からのレスポンスはデフォルトではキャッシュされません。
- TypeScript
- JavaScript
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const 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'
ただし、通常はcookies
、headers
、またはページのpropsからのsearchParams
を読み取る関数を使用するため、ページは自動的に動的にレンダリングされます。この場合、明示的にforce-dynamic
を使用する必要はありません。
ORMまたはデータベースを使用したサーバーでのデータ取得
このコンポーネントはブログ投稿のリストを取得して表示します。データベースからのレスポンスはデフォルトではキャッシュされませんが、追加の設定でキャッシュできます。
- TypeScript
- JavaScript
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
import { db, posts } from '@/lib/db'
export default async function Page() {
const 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'
ただし、通常はcookies
、headers
、またはページのpropsからのsearchParams
を読み取る関数を使用するため、ページは自動的に動的にレンダリングされます。この場合、明示的にforce-dynamic
を使用する必要はありません。
クライアントでのデータ取得
まずはサーバーサイドでデータを取得することをお勧めします。
しかし、クライアントサイドでのデータ取得が理にかなう場合もあります。これらのシナリオでは、useEffect
で手動でfetch
を呼び出す(推奨されません)か、クライアント取得のためにコミュニティの人気のあるReactライブラリ(SWRやReact Queryなど)を利用することができます。
- TypeScript
- JavaScript
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://api.vercel.app/blog')
const 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>
)
}
'use client'
import { useState, useEffect } from 'react'
export function Posts() {
const [posts, setPosts] = useState(null)
useEffect(() => {
async function fetchPosts() {
const res = await fetch('https://api.vercel.app/blog')
const 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
を実行する際にページをプリレンダリングできるようにします。
- TypeScript
- JavaScript
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>
)
}
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はgenerateMetadata
やgenerateStaticParams
のようなAPIを使用して、page
で取得した同じデータを使用する必要があります。
fetch
を使用している場合、リクエストはcache: 'force-cache'
を追加することでメモ化できます。これにより、同じURLを同じオプションで安全に呼び出すことができ、1つのリクエストのみが行われます。
Good to know:
- 以前のバージョンのNext.jsでは、
fetch
を使用するとデフォルトでcache
の値がforce-cache
でした。これはバージョン15で変更され、デフォルトがcache: no-store
になりました。
- TypeScript
- JavaScript
import { notFound } from 'next/navigation'
interface Post {
id: string
title: string
content: string
}
async function getPost(id: string) {
const res = await fetch(`https://api.vercel.app/blog/${id}`, {
cache: 'force-cache',
})
const post: Post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
const posts = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
}).then((res) => res.json())
return posts.map((post: Post) => ({
id: String(post.id),
}))
}
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return {
title: post.title,
}
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
import { notFound } from 'next/navigation'
async function getPost(id) {
const res = await fetch(`https://api.vercel.app/blog/${id}`)
const post = await res.json()
if (!post) notFound()
return post
}
export async function generateStaticParams() {
const posts = await fetch('https://api.vercel.app/blog').then((res) =>
res.json()
)
return posts.map((post) => ({
id: String(post.id),
}))
}
export async function generateMetadata({ params }) {
const { id } = await params
const post = await getPost(id)
return {
title: post.title,
}
}
export default async function Page({ params }) {
const { id } = await params
const post = await getPost(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つのデータ取得パターン:並列と逐次に注意する必要があります。
- 逐次:コンポーネントツリー内のリクエストが互いに依存している。これにより、読み込み時間が長くなる可能性があります。
- 並列:ルート内のリクエストが積極的に開始され、同時にデータを読み込みます。これにより、データの読み込みにかかる総時間が短縮されます。
逐次データ取得
ネストされたコンポーネントがあり、各コンポーネントが独自のデータを取得する場合、これらのデータリクエストがメモ化されていない限り、データ取得は逐次的に行われます。
このパターンを望む場合もあります。たとえば、1つのフェッチが他の結果に依存している場合です。たとえば、Playlists
コンポーネントはartistID
propに依存しているため、Artist
コンポーネントがデータのフェッチを完了するまでデータのフェッチを開始しません:
- TypeScript
- JavaScript
export default async function Page({
params,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
// アーティスト情報を取得
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>
)
}
export default async function Page({ params }) {
const { username } = await params
// アーティスト情報を取得
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 }) {
// アーティストIDを使用してプレイリストを取得
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
loading.js
(ルートセグメント用)またはReact <Suspense>
(ネストされたコンポーネント用)を使用して、結果がストリーミングされる間に即時の読み込み状態を表示できます。
これにより、データリクエストによってルート全体がブロックされるのを防ぎ、ユーザーは準備ができているページの部分と対話できるようになります。
並列データ取得
デフォルトでは、レイアウトとページセグメントは並列にレンダリングされます。これにより、リクエストが並行して開始されます。
ただし、async
/await
の性質上、同じセグメントまたはコンポーネント内で待機されたリクエストは、その下のリクエストをブロックします。
データを並列に取得するには、データを使用するコンポーネントの外でリクエストを積極的に開始することで、時間を節約できます。これにより、両方のリクエストが並行して開始されますが、両方のプロミスが解決されるまでユーザーにはレンダリング結果が表示されません。
以下の例では、getArtist
とgetAlbums
関数がPage
コンポーネントの外で定義され、Promise.all
を使用してコンポーネント内で開始されます:
- TypeScript
- JavaScript
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,
}: {
params: Promise<{ username: string }>
}) {
const { username } = await params
const artistData = getArtist(username)
const albumsData = getAlbums(username)
// 両方のリクエストを並行して開始
const [artist, albums] = await Promise.all([artistData, albumsData])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
import Albums from './albums'
async function getArtist(username) {
const res = await fetch(`https://api.example.com/artist/${username}`)
return res.json()
}
async function getAlbums(username) {
const res = await fetch(`https://api.example.com/artist/${username}/albums`)
return res.json()
}
export default async function Page({ params }) {
const { username } = await params
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つの方法は、ユーティリティ関数を作成してブロッキングリクエストの上で積極的に呼び出すことによって、プリロードパターンを使用することです。たとえば、checkIsAvailable()
は<Item/>
のレンダリングをブロックしますが、preload()
をその前に呼び出すことで<Item/>
のデータ依存関係を積極的に開始できます。<Item/>
がレンダリングされる頃には、そのデータはすでに取得されています。
preload
関数はcheckIsAvailable()
の実行をブロックしないことに注意してください。
- TypeScript
- JavaScript
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)
// ...
}
- TypeScript
- JavaScript
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// アイテムデータの読み込みを開始
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 }) {
const { id } = await params
// アイテムデータの読み込みを開始
preload(id)
// 別の非同期タスクを実行
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
Good to know: "preload"関数はパターンであり、APIではないため、任意の名前を持つことができます。
React cache
とserver-only
を使用したプリロードパターン
cache
関数、preload
パターン、およびserver-only
パッケージを組み合わせて、アプリ全体で使用できるデータ取得ユーティリティを作成できます。
- TypeScript
- JavaScript
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
のエクスポートは、レイアウト、ページ、または他のコンポーネントによって使用され、アイテムのデータが取得されるタイミングを制御できます。
Good to know:
- サーバーデータ取得関数がクライアントで使用されないようにするために、
server-only
パッケージを使用することをお勧めします。
クライアントに機密データが公開されるのを防ぐ
Reactの汚染API、taintObjectReference
とtaintUniqueValue
を使用して、オブジェクトインスタンス全体や機密値がクライアントに渡されるのを防ぐことをお勧めします。
アプリケーションで汚染を有効にするには、Next.js Configのexperimental.taint
オプションをtrue
に設定します:
module.exports = {
experimental: {
taint: true,
},
}
次に、experimental_taintObjectReference
またはexperimental_taintUniqueValue
関数に汚染したいオブジェクトまたは値を渡します:
- TypeScript
- JavaScript
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
}
- TypeScript
- JavaScript
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のためエラーが発生します
/>
)
}