React Server Componentでのデータフェッチング
React Server Componentsでasync関数を使ってデータ取得し、Next.js 15/16のキャッシュ既定値とPromise.allでのウォーターフォール回避を解説。
React Server Component内でデータをフェッチするには、コンポーネントをasync関数にして、関数本体で直接リクエストをawaitします。useEffectも、ローディング状態も、クライアントサイドのAPIラウンドトリップも不要です。これはNext.js App Routerにおけるデフォルトのモデルであり、クライアントReactから引き継がれた2つの習慣を逆転させます。すなわち、データフェッチングがレンダリング内に移動し、最大の落とし穴はキャッシングになります。このキャッシングの方向性はNext.js 15で変わりました。本記事では、現在のパターン、Next.js 16におけるキャッシングの実態、"use server"がServer Componentと無関係である理由、そしてリクエストのウォーターフォールを避ける方法について解説します。
重要なポイント
- Server Componentでは、コンポーネントを
asyncにしてリクエストをawaitすることでデータをフェッチします。Server Componentはサーバー上でレンダリングされるため、認証情報やクエリロジックはクライアントバンドルに含まれません。そのため、ORMを使用してデータベースに直接クエリを実行することができます。 - 現在のNext.jsでは、デフォルトでfetchリクエストはキャッシュされません。これは、多くのチュートリアルが依然として説明しているNext.js 13/14の動作とは逆です。
- キャッシュを使用するには明示的にオプトインが必要です。従来のモデルでは
{ cache: 'force-cache' }または{ next: { revalidate } }を使用し、cacheComponents: trueが設定されている場合はuse cacheディレクティブを使用します。 - Server Componentに対応するディレクティブは存在しません。“use server”ディレクティブはServer Functions(Server Actions)に使用されるものであり、別の機能です。
- ウォーターフォールを避けるために、独立したフェッチを
awaitなしで開始し、Promise.allで解決します。
Server Componentでデータをフェッチするには?
基本的なパターンは1つの操作に集約されます。コンポーネントをasync関数にして、リクエストをawaitするだけです。App RouterではレイアウトとページにServer Componentがデフォルトで使用されるため、この動作を得るためにディレクティブは必要ありません。asyncコンポーネントはServer Componentの機能であり、レンダリング内でawaitを使用することができます。
// app/blog/page.tsx
export default async function Page() {
const res = await fetch('https://api.vercel.app/blog')
const posts = await res.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
このコードはサーバー上でのみ実行されるため、APIレイヤーをスキップしてデータソースに直接クエリを実行できます。ORMやデータベースクライアントを使用してデータベースクエリを安全に実行できますが、リクエストが適切に認証・認可されていることを確認する必要があります。
// app/blog/page.tsx
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>
)
}
これを、置き換えようとしているクライアントのパターンと比較してみましょう。useEffectでフェッチし、結果をuseStateに保存し、フェッチロジック(シークレット情報を含む)をブラウザに送信するというアプローチです。このアプローチではページ読み込み後にクライアントとサーバー間の余分なラウンドトリップが発生しますが、サーバー上でawaitすることでそれを排除できます。
Discover how at OpenReplay.com.
fetchはデフォルトでキャッシュされない — Next.js 15/16の変更点
現在のNext.jsでは、fetchはデフォルトでキャッシュされません。多くの古いコンテンツが逆のことを述べているため、明確に述べておく価値があります。現在のNext.jsのfetchリファレンスによると、デフォルトはauto no cacheです。つまり、Next.jsはリクエストごとにリモートサーバーからリソースをフェッチします。以前のキャッシングモデルも同様の動作をしていました。デフォルトではfetchリクエストはキャッシュされず、個々のリクエストをキャッシュするにはcacheオプションを'force-cache'に設定してオプトインします。Next.js 13/14では、修飾子のないfetchはデフォルトでキャッシュ(force-cache)されていたため、それに依存しているチュートリアルは古い情報となっています。
以前のモデルでキャッシュを使用するには、リクエストごとにオプトインします。
// 手動で再検証されるまでキャッシュ
await fetch('https://api.example.com/posts', { cache: 'force-cache' })
// キャッシュされたデータを提供し、最大3600秒ごとに再検証
await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } })
Next.js 16では、Cache Componentsという2つ目のモデルも導入されており、そのオプトインはuse cacheディレクティブです。注意すべき点は、use cacheはCache Componentsの機能であり、有効にするにはnext.config.tsファイルにcacheComponentsオプションを追加する必要があります。このフラグがなければuse cacheは何もせず、上記のfetchオプションにフォールバックします。
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true }
export default nextConfig
import { cacheLife } from 'next/cache'
export async function getPosts() {
'use cache'
cacheLife('hours')
const res = await fetch('https://api.example.com/posts')
return res.json()
}
ここではuse cacheディレクティブがasync関数とコンポーネントの戻り値をキャッシュし、cacheLife()でキャッシュの有効期間を設定します。なお、Next.js 16ではunstable_cacheはuse cacheディレクティブに置き換えられたため、新しいコードでは使用しないでください。
| Cache Components OFF(以前のモデル) | Cache Components ON(cacheComponents: true) | |
|---|---|---|
fetchのデフォルト | キャッシュなし | キャッシュなし |
| fetchのキャッシュ | { cache: 'force-cache' } | use cacheディレクティブ |
| 時間ベースの再検証 | { next: { revalidate: n } } | cacheLife()プロファイル |
| fetch以外のデータのキャッシュ | React.cache / ルート設定 | 関数へのuse cache |
キャッシングとは独立した2つの動作があります。1つ目は、同じURLとオプションを使用したGETのfetchリクエストは、サーバーレンダリングパス中に自動的にメモ化されるため、複数のコンポーネントで同じfetchを呼び出しても、Next.jsは1回だけ実行して結果を共有します。2つ目は、fetch以外のデータソースの場合、Reactのcache()でその呼び出しをラップします。React.cacheは現在のリクエストのみにスコープされ、各リクエストは独自のメモ化スコープを持ち、リクエスト間で共有されません。
“use server”はServer Componentを作成しない
RSCの初心者がつまずく点があるとすれば、このディレクティブです。Server Componentは”use server”で示されるという誤解がよくありますが、Server Componentにはディレクティブが存在せず、“use server”ディレクティブはServer Functions(Server Actions)に使用されます。Server ComponentはApp Routerではデフォルトであり、ディレクティブのないファイルがそれに該当します。"use server"はServer Functions(Server Actions)をマークするものであり、Reactのリファレンスによると、Server Functionsはサーバーサイドの状態を更新するミューテーション向けに設計されており、データフェッチングには推奨されません。データの読み取りには通常のasync Server Componentを使用し、データの書き込みには"use server"を使用してください。
ウォーターフォールを避け、低速なデータをストリームする
リクエストを1つずつawaitすると、順次ウォーターフォールが発生します。コンポーネント内では、複数のasync/awaitリクエストが順番に配置されると依然として順次実行される可能性があります。fetchを呼び出して複数のリクエストを開始し、Promise.allでawaitすることで並列実行できます。awaitなしで関数を呼び出すと、即座に実行が開始されます。
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>
}
本当に低速なデータの場合は、ルートをブロックしないようにしましょう。低速なデータリクエストがあると、すべてのデータがフェッチされるまでルート全体のレンダリングがブロックされます。読み込み時間を改善するには、ページをチャンクに分割してクライアントに段階的に送信します。低速なサブツリーを<Suspense>でラップ(またはloading.jsを追加)し、優先度の低いデータについてはサーバーでPromiseを開始してuse APIでクライアントから読み取ります。
// Server Component — 注意: commentsPromiseはawaitされていない
import { Suspense } from 'react'
import Comments from './comments'
export default async function Page({ id }: { id: string }) {
const commentsPromise = getComments(id)
return (
<Suspense fallback={<p>Loading comments…</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
)
}
// Client Component
'use client'
import { use } from 'react'
export default function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return comments.map((c) => <p key={c.id}>{c.text}</p>)
}
サーバーでPromiseを開始し、use APIでクライアントから待機します。asyncコンポーネントはクライアントでサポートされていないため、useでPromiseをawaitします。問題が発生した場合、その障害はユーザーが見ている場所にのみ現れます。コンテンツに解決されないSuspenseフォールバック、またはハイドレーション後に届くストリームされたチャンクなどです。セッションリプレイを使用することで、その瞬間にブラウザがレンダリングしていた内容を正確に再現できます。
シークレット情報をサーバーサイドに保持する
サーバーでフェッチされたデータはClient Componentにpropsとして渡されますが、これらのpropsはシリアライズ可能である必要があります。関数やクラスインスタンスではなく、プレーンなデータを渡してください。Server Componentのコードは送信されないため、シークレット情報はデフォルトで保護されますが、共有モジュールからリークする可能性があります。Next.jsでは、NEXT_PUBLIC_プレフィックスが付いた環境変数のみがクライアントバンドルに含まれます。プレフィックスのない変数は、Next.jsが空文字列に置き換えます。シークレット情報を含むモジュールを保護するには、ファイルの先頭でserver-onlyパッケージをインポートすることで、誤ってクライアントからインポートされた場合にビルド時にエラーが発生するようにします。React ContextはServer Componentには使用できません。'use client'プロバイダーでラップし、そのプロバイダーをサーバーレイアウト内でレンダリングしてください。
考え方の転換は小さいですが完全なものです。fetchをasyncコンポーネントに移動し、awaitして、キャッシングを自動的に行われるものではなくオプトインするものとして扱います。まず1つのuseEffectデータフェッチをasync Server Componentに移行し、リクエストがサーバーサイドで実行されることを確認してから、ルートごとに{ next: { revalidate } }またはuse cacheディレクティブでデータをキャッシュするかどうかを決定してください。
よくある質問
Next.js 16ではfetchはデフォルトでキャッシュされますか?
いいえ。Next.js 15以降(16を含む)では、fetchはデフォルトでキャッシュされず、リクエストごとにオリジンにアクセスします。これはNext.js 13/14とは逆で、以前は修飾子のないfetchがデフォルトでforce-cacheによりキャッシュされていました。以前のモデルでキャッシュするには、cacheをforce-cacheに設定するか、next revalidateの値を指定してリクエストごとにオプトインします。デフォルトでキャッシュされると主張している古いチュートリアルは情報が古くなっています。
fetchのキャッシュにおいて、use cacheとnext revalidateの違いは何ですか?
これらは2つの異なるモデルに属しています。next revalidateオプションとforce-cacheは、追加設定不要のデフォルトキャッシングモデルで機能します。use cacheディレクティブはCache Componentsの機能であり、next.config.tsでcacheComponentsをtrueに設定した場合にのみ有効になります。その有効期間はcacheLifeで制御します。このフラグがない場合、use cacheは何もしないため、キャッシュするつもりだったfetchが静かにリクエストごとにオリジンにアクセスし続けます。
Server ComponentでuseEffectを使用してデータをフェッチできますか?
いいえ。useEffectはブラウザでのみ実行されるため、サーバー上でのみレンダリングされるServer Component内では実行できません。Server Componentではコンポーネントをasyncにして、ローディング状態やクライアントのラウンドトリップなしに、レンダリング内でリクエストを直接awaitします。クライアントサイドのフェッチが必要な場合は、use clientディレクティブを追加してClient Componentにするか、サーバーでPromiseを開始してReactのuse APIでクライアントから読み取ります。
use serverディレクティブはコンポーネントをServer Componentにしますか?
いいえ。Server Componentにはディレクティブが存在せず、App Routerではデフォルトとなっているため、ディレクティブのないファイルはすでにServer Componentです。use serverディレクティブはServer Functions(Server Actionsとも呼ばれる)をマークするものであり、サーバーサイドの状態を更新するミューテーション向けに設計されており、データフェッチングには推奨されません。データの読み取りには通常のasync Server Componentを使用し、データの書き込みにはuse serverを使用してください。
並列リクエストにPromise.allを使用している場合、1つのfetchが失敗するとどうなりますか?
Promise.allはいずれか1つのリクエストが拒否された時点で即座に拒否し、他のリクエストの結果を破棄してレンダリング全体を失敗させます。1つのリクエストが失敗しても他に影響を与えないようにするには、代わりにPromise.allSettledを使用します。これはすべてのリクエストのステータスで解決し、個別に失敗を処理できます。fetchをawaitなしで開始して並列実行し、失敗の処理方法に応じてPromise.allまたはPromise.allSettledを選択してください。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k