👾

App Routerの基本的なデータフェッチング手法

2024/08/16に公開
2

はじめに

こんにちは!
株式会社BLUEISH エンジニアの佐々木(@osasasasa22)です。

ここ十数年のWebアプリ開発では、ブログ記事の表示やユーザーのプロフィール情報取得など、外部データを取得して表示することが当たり前になってますよね。

Next.jsのApp Routerは、そんなデータフェッチをより直感的で柔軟に行うことができるようになっている+キャッシングが強化されたり、Server Actionsという新たな機能が追加されたり。。と、大きく進化ししています。

ということで、今回はApp Routerにおけるさまざまなデータフェッチの手法について、これからNext.jsを学ぶ方、「Pages Routerを使ったことはあるけどApp Routerはまだ〜」という方向けにお伝えしていきたいと思います📣

https://zenn.dev/blueish/articles/4b2ae3781ade57

サーバーサイドでのデータフェッチ

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を定義し、インポートして使用します。

actions.ts
'use server'
export async function create() {
  // 処理内容
}
client-component.jsx
import { create } from './actions'

<form>要素からの呼び出し

Server Actionsは<form>要素のaction属性を使用して簡単に呼び出すことができます。
フォームのaction属性にServer Actionを直接指定するだけで、フォームが送信されるときに自動的にServer Actionが実行されます。

app/actions.ts
'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')
}
app/components/NewPostForm.tsx
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関数を使用してビルド時にデータを取得し、静的なページを生成してみましょう。

pages/ssg-example.tsx
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では、コンポーネントを非同期関数として定義し、その中で直接データフェッチを行います。

app/ssg-example/page.tsx
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を実装します。

pages/isr-example.tsx
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を実装します。

app/isr-example/page.tsx
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. オンデマンドの再検証

特定のイベント(フォーム送信など)が起きたときにデータを再検証する方法です。
オンデマンドの再検証には、revalidateTagrevalidatePathを使用します。

revalidateTagは、特定のタグが付いたデータを再検証するというものです。
複数の場所で使われている同じタグのデータを一度に更新できるので、商品カテゴリー、ユーザーデータなど、特定の種類のデータを更新する場合に活用できそうですね。

// データ取得時
fetch('https://...', { next: { tags: ['tag-name'] } })

// 再検証時
revalidateTag('tag-name')

revalidatePathは、特定のURLパスに関連するデータを再検証します。

revalidatePath('/blog')

部分的なパスやワイルドカードも使えるので、ブログ記事の更新後に関連ページを全て更新する場合などに利用できそうです。

キャッシュが効かないケース

キャッシュが効かない、もしくは意図的に効かせない方法も存在します。

たとえば、fetchリクエストでcache: 'no-store'を指定する、ゼロ秒で再検証を設定する、POSTメソッドを使用する方法などがあります。

fetch('https://...', { cache: 'no-store' })

また、動的な情報を使う場合や、全体のページを動的に設定する場合もキャッシュされません。

詳しく知りたい方は公式ドキュメントを覗いてみてください💡
https://nextjs.org/docs/app/building-your-application/data-fetching/caching-and-revalidating

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

akfm_satoakfm_sato

また、動的な情報を使う場合や、全体のページを動的に設定する場合もキャッシュされません。

ページをdynamic renderingにしても、Data Cacheはデフォルトで有効だと思います。
例えばページ内に2つのfetchがあって、2つ目にno storeをつけた場合、ページ自体はdynamic renderingになりますが1つ目のfetchはキャッシュが有効なはずです!
又聞きですが、Next.jsはbuild時に「一旦実行して様々な情報を集め、可能なところまでキャッシュする」という戦略を取っているようです

佐々木 美遥 | BLUEISH Engineer佐々木 美遥 | BLUEISH Engineer

@akfm_sato さん
ご返信遅くなり申し訳ございません🙇🏻‍♂️

ご指摘いただきありがとうございます!
該当文章に訂正線を引き、以下を追記させていただきましたmm