App Routerの基本的なデータフェッチング手法
はじめに
こんにちは!
株式会社BLUEISH エンジニアの佐々木(@osasasasa22)です。
ここ十数年のWebアプリ開発では、ブログ記事の表示やユーザーのプロフィール情報取得など、外部データを取得して表示することが当たり前になってますよね。
Next.jsのApp Routerは、そんなデータフェッチをより直感的で柔軟に行うことができるようになっている+キャッシングが強化されたり、Server Actionsという新たな機能が追加されたり。。と、大きく進化ししています。
ということで、今回はApp Routerにおけるさまざまなデータフェッチの手法について、これからNext.jsを学ぶ方、「Pages Routerを使ったことはあるけどApp Routerはまだ〜」という方向けにお伝えしていきたいと思います📣
サーバーサイドでのデータフェッチ
App Routerでは、サーバーサイドでのデータフェッチがより自然で効率的になりました。
従来のPages Routerと比較すると、サーバーコンポーネントがデフォルトで採用され、データフェッチング処理にも変化がうまれたため、そちらについて解説していきます。
【Pages Router】
-
getServerSideProps
,getStaticProps
,getInitialProps
などの特別な関数を使用 - これらの関数内で
fetch
を使用してデータを取得 - データは
props
としてページコンポーネントに渡される
// getServerSideProps: ページのリクエスト時に毎回サーバーサイドで実行される関数
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/posts')
const data = await res.json()
return { props: { data } }
}
【App Router】
- サーバーコンポーネント内で直接fetchを使用可能
- Pages Routerで必要だった特別な関数は不要で、
async/await
が直接使用できる - コンポーネントのレンダリングフローの一部としてデータフェッチが行われる
async function getData() {
// fetch関数はサーバーサイドで実行されるため、初期ページロードが高速化される
const res = await fetch('https://api.example.com/posts')
return res.json()
}
// デフォルトでサーバーコンポーネントとして扱われる
// asyncキーワードを使用可能で、直接データフェッチが可能
export default async function Page() {
const data = await getPost()
return <div>{data.title}</div>
}
クライアントサイドでのデータフェッチ
ユーザーのアクションに基づくデータ更新や、リアルタイムデータの表示、クライアントの状態に依存するデータの取得を行いたいときは、クライアントサイドでのデータフェッチが必要になります。
App Routerでは、デフォルトでサーバーコンポーネントとなるため、 'use client'
ディレクティブを使用してクライアントコンポーネントを明示的に宣言する必要があります。
それ以外はPages Routerと大きな違いはありません。
'use client' // クライアントコンポーネントであることを明示
import { useState, useEffect } from 'react'
export default function ClientSidePostList() {
const [posts, setPosts] = useState([])
// クライアントサイドでのfetch:ユーザーアクション後やリアルタイム更新に適している
useEffect(() => {
async function fetchPosts() {
const res = await fetch('/api/posts')
const data = await res.json()
setPosts(data)
}
fetchPosts()
}, [])
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Server Actions
Server Actionsは、Next.jsのApp Routerで導入された新機能です🆕
従来のPages Routerには存在しなかったアプローチで、サーバーサイドの処理を効率的に行うことを可能にする、App Router独自の機能です。
App Router内で定義され、クライアントサイドのコンポーネントから呼び出されることで、サーバーサイド処理を安全かつ簡単に実行できます。
基本的な使い方
Server Actionsは、"use server"
ディレクティブを使用して定義します。
関数の先頭、またはファイルの先頭に配置できます。
Server Componentsでの使用
Server Componentsでは、Server Actionを直接定義して使用することができます。
export default function Page() {
async function create() {
'use server'
// 処理内容
}
return (/* コンポーネントの内容 */)
}
Client Componentsでの使用
Client Componentsでは、別ファイルでServer Actionを定義し、インポートして使用します。
'use server'
export async function create() {
// 処理内容
}
import { create } from './actions'
<form>
要素からの呼び出し
Server Actionsは<form>
要素のaction
属性を使用して簡単に呼び出すことができます。
フォームのaction
属性にServer Actionを直接指定するだけで、フォームが送信されるときに自動的にServer Actionが実行されます。
'use server'
import { revalidatePath } from 'next/cache'
export async function addPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// データベースに新しい投稿を追加する処理
// ...
revalidatePath('/blog')
}
import { addPost } from '../actions'
export default function NewPostForm() {
return (
<form action={addPost}> // action属性にServer Actionを直接指定
<input name="title" type="text" placeholder="タイトル" />
<textarea name="content" placeholder="内容" />
<button type="submit">投稿</button>
</form>
)
}
SSG(Static Site Generation)とISR(Incremental Static Regeneration)の活用
さて、ここからは、データフェッチでもビルドする時にデータフェッチをし、静的なサイトを生成したり、更新したりするSSGとISRについて解説していきます。
App Routerは、SSGとISRの概念を維持しつつ、より直感的でわかりやすい実装方法を提供しています。
理解が曖昧な方も多いと思うので、まずはSSGとISRについておさらいしましょう。
SSGとISRの基本
SSG (Static Site Generation)とは
静的サイト生成(SSG)は、ウェブサイトのコンテンツをビルド時に生成する手法です。
SSGでは、ビルド時にすべてのページのHTMLが生成されるので、ページの読み込み速度が非常に速くなります。
また、検索エンジンのクローラーが完全に生成されたHTMLを読み取れるため、サイトのコンテンツがより正確にインデックス化されやすくなるので、SEOの観点からも有利とされています。
ISR(Incremental Static Regeneration)とは
ISRでは、初回はSSGと同様にビルド時にページが生成されますが、その後は設定された間隔、またはオンデマンドでページが再生成されます。
ISRの大きな利点は、SSGの高速な読み込みやSEOの向上といった特徴を維持しながら、コンテンツを定期的に更新できることです。
また、全ページを一度に再ビルドする必要がないため、大規模サイトでも効率的に運用できます。
SSGの実装比較
Next.jsのPages Routerでは、SSGとISRの実装に特定の関数やオプションを使用します。
Pages RouterでのSSG実装例
Pages Routerでは、getStaticProps
関数を使用してSSGを実装します。
getStaticProps
関数を使用してビルド時にデータを取得し、静的なページを生成してみましょう。
import { GetStaticProps, InferGetStaticPropsType } from 'next'
type Data = {
title: string
}
// getStaticProps: ビルド時に実行される関数
// この関数内でのデータフェッチはサーバーサイドで行われる
export const getStaticProps: GetStaticProps<{ data: Data }> = async () => {
const res = await fetch('https://api.example.com/data')
const data: Data = await res.json()
return {
props: { data },
}
}
// getStaticPropsから返されたデータをpropsとして受け取る
function SSGPage({ data }: InferGetStaticPropsType<typeof getStaticProps>) {
return <div>{data.title}</div>
}
export default SSGPage
App RouterでのSSG実装例
App Routerでは、コンポーネントを非同期関数として定義し、その中で直接データフェッチを行います。
type Data = {
title: string
}
// データ取得関数
// App Routerでは、別個の関数としてデータフェッチロジックを定義できる
async function getData(): Promise<Data> {
// fetch APIを使用してデータを取得
// App Routerでは、デフォルトでこのfetchリクエストが自動的にキャッシュされる
const res = await fetch('https://api.example.com/data')
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
// App Routerでは、コンポーネントを直接asyncにできる
export default async function SSGPage() {
const data = await getData()
return <div>{data.title}</div>
}
App Routerでは、デフォルトで静的レンダリングが適用されるので、特別な設定は必要ありません🙆🏻♂️
ISRの実装比較
Pages RouterでのISR実装例
Pages Routerでは、getStaticProps
関数の戻り値にrevalidate
プロパティを追加してISRを実装します。
import { GetStaticProps, InferGetStaticPropsType } from 'next'
type Data = {
title: string
}
// getStaticProps: ビルド時とre-validation時に実行される関数
export const getStaticProps: GetStaticProps<{ data: Data }> = async () => {
const res = await fetch('https://api.example.com/data')
const data: Data = await res.json()
return {
props: { data },
revalidate: 60, // 60秒ごとに再検証
}
}
function ISRPage({ data }: InferGetStaticPropsType<typeof getStaticProps>) {
return <div>{data.title}</div>
}
export default ISRPage
App RouterでのISR実装例
App Routerでは、fetch
関数の第二引数にnext.revalidate
オプションを追加してISRを実装します。
type Data = {
title: string
}
// データ取得関数
async function getData(): Promise<Data> {
// fetch関数の第二引数にnext.revalidateオプションを追加することで、ISRを実現
const res = await fetch('https://api.example.com/data', { next: { revalidate: 60 } }) // 60秒ごとに再検証
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
// App Routerでは、コンポーネントを直接asyncにできる
export default async function ISRPage() {
// データ取得を直接コンポーネント内で行える
const data = await getData()
return <div>{data.title}</div>
SSGとISRに関する解説は以上です。
App Routerでは、getStaticProps
のような特別な関数を使用せずに、コンポーネント内で直接データフェッチができるようになりました。
また、fetch
関数の第二引数でキャッシュの動作を細かく制御できるようにもなりました。
キャッシュについては次の節で解説します。
キャッシュと再検証
Next.js は標準の fetch API を拡張して、サーバー側でデータを取得するときにキャッシュや再検証の設定ができるようにしています。
(再検証とは、キャッシュされたデータを更新するプロセスのことです。)
再検証の方法
再検証には2つの方法があります。
1. 時間ベースの再検証
一定時間が経過したら自動的にデータを更新する方法です。
この方法は、変更頻度が低く、鮮度がそれほど重要でないデータに有効です。
fetch('https://...', { next: { revalidate: 3600 } })
上記コードでは、3600秒(1時間)ごとにデータが再検証されます。
2. オンデマンドの再検証
特定のイベント(フォーム送信など)が起きたときにデータを再検証する方法です。
オンデマンドの再検証には、revalidateTag
やrevalidatePath
を使用します。
revalidateTag
は、特定のタグが付いたデータを再検証するというものです。
複数の場所で使われている同じタグのデータを一度に更新できるので、商品カテゴリー、ユーザーデータなど、特定の種類のデータを更新する場合に活用できそうですね。
// データ取得時
fetch('https://...', { next: { tags: ['tag-name'] } })
// 再検証時
revalidateTag('tag-name')
revalidatePath
は、特定のURLパスに関連するデータを再検証します。
revalidatePath('/blog')
部分的なパスやワイルドカードも使えるので、ブログ記事の更新後に関連ページを全て更新する場合などに利用できそうです。
キャッシュが効かないケース
キャッシュが効かない、もしくは意図的に効かせない方法も存在します。
たとえば、fetch
リクエストでcache: 'no-store'
を指定する、ゼロ秒で再検証を設定する、POSTメソッドを使用する方法などがあります。
fetch('https://...', { cache: 'no-store' })
また、動的な情報を使う場合や、全体のページを動的に設定する場合もキャッシュされません。
詳しく知りたい方は公式ドキュメントを覗いてみてください💡
Suspenseを用いたローディング状態の管理
最後に、データの読み込み中の表示について見ていきたいと思います。
※引用: Next.js Docs 「Loading UI and Streaming」https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense (参照2024-08-15)
従来は、ローディング状態を管理するために状態管理ライブラリを使ったり、複雑なコードを書いたりする必要がありましたが、Reactの機能であるSuspenseを使えば、数行のコードでスッキリと書くことができます。
例えば、ブログページの記事リストをSuspenseを使って表示してみましょう。
import { Suspense } from 'react';
import PostList from '@/components/PostList';
export default function BlogPage() {
return (
<Suspense fallback={<div>記事リストを読み込み中...</div>}>
<PostList />
</Suspense>
);
}
たったこれだけで、記事リストを取得している間は「記事リストを読み込み中...」というメッセージが表示され、データ取得が完了すると記事リストが表示されます。
また、複数のコンポーネントを別々にロードすることで、ページの一部から徐々に表示することができるので、ユーザーは、すべてのコンテンツがロードされるのを待つことなく、利用可能な部分から操作を開始できます。
ブログページで記事リストとプロフィールを別々にロードさせたい場合には、以下のように書きます。
import { Suspense } from 'react';
import PostList from '@/components/PostList';
import Profile from '@/components/Profile';
export default function BlogPage() {
return (
<div>
<Header /> {/* データを必要としない部分は即座に表示 */}
<div className="layout">
<Suspense fallback={<div>記事リストを読み込み中...</div>}>
<PostList /> {/* データフェッチが必要な部分 */}
</Suspense>
<Suspense fallback={<div>プロフィールを読み込み中...</div>}>
<Profile /> {/* 別のデータフェッチが必要な部分 */}
</Suspense>
</div>
</div>
);
}
Suspenseをうまく使い分けることで、UXを向上させることができそうですね。
まとめ
いかがでしたでしょうか?
データフェッチは奥が深いトピックですが、基本を押さえることで、多くの場面で適切な選択ができるようになります。ここではごく一部の主要機能にしか触れられていませんが、学習のきっかけになると幸いです!
最後までお読みいただきありがとうございました。
(次回は、ネストされたレイアウトの実装など、高度なレイアウト設計について解説予定です🔥)
Discussion
ページをdynamic renderingにしても、Data Cacheはデフォルトで有効だと思います。
例えばページ内に2つのfetchがあって、2つ目にno storeをつけた場合、ページ自体はdynamic renderingになりますが1つ目のfetchはキャッシュが有効なはずです!
又聞きですが、Next.jsはbuild時に「一旦実行して様々な情報を集め、可能なところまでキャッシュする」という戦略を取っているようです
@akfm_sato さん
ご返信遅くなり申し訳ございません🙇🏻♂️
ご指摘いただきありがとうございます!
該当文章に訂正線を引き、以下を追記させていただきましたmm