🌎

App Routerでfetchを使えない場合のISRについて

2023/10/13に公開

はじめに

App RouterでISRができるfetch関数が用意されていますが、HTTPベースのリクエストにしか対応していません。firestoreやsupabaseからデータを取得したい場合にどうすべきかという記事が見当たらなかったので、投稿しました。
もっといい方法を知っている方がいれば、教えていただきたいです。(こっちが本題)

export default async function Page() {
 const res = await fetch('https://api.example.com/...', { next: { revalidate: 3600 } })
  const data = res.json()

  return <Home data={data} />
}

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-fetch

結論

以下のようにすると、ISRになります。
60秒間はキャッシュされたデータが渡されるため、その間はデータベースに対してクエリが行われることはありません。

page.tsx
import { getData } from './utils'

export const revalidate = 60

export default async function Page() {
 const data = await getData('id')

  return <Home data={data} />
}
utils.tsx
import { cache } from 'react'

export const getData = cache(async (id: string) => {
  const data = await db.query('...')
  return data
})

以下のページに記載がありました。
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-third-party-libraries

If the segment is static (default), the output of the request will be cached and revalidated as part of the route segment.

セグメントが静的 (デフォルト) の場合、リクエストの出力はルート セグメントの一部としてキャッシュされ、再検証されます。

キャッシュするページの容量が大きそうな場合は、cacheMaxMemorySizeを設定しておきましょう。
デフォルトは50Mです。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  cacheMaxMemorySize: 50 * 1024 * 1024 // 50M
}

module.exports = nextConfig

cache

公式ページの以下のページにreactのcacheを使うと、fetchを利用できないサードパーティーライブラリーからのデータ取得ができると記載があります。
cacheを使わないと、revalidateで指定した時間がたっていなくてもデータの再検証がされている場合があり、使った方が安定するので利用しています。
page.tsx内に記載するrevalidateはpageの生成間隔というよりは、ページ生成するためのデータをキャッシュする間隔なのかなと推測しています。
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-third-party-libraries
cacheを使うと以下のようにlayout.tsxとpage.tsxで同じデータを取得するときもキャッシュされるので、dbへのクエリは1回で済みます。
ただ、Dynamic Routesを使用すると、revalidateで指定した値が無視されてページアクセスのたびにクエリが走ります。

app/item/[id]/layout.tsx
import { getData } from './utils'

export const revalidate = 60

export default async function Layout({
  params: { id },
}: {
  params: { id: string }
}) {
  const item = await getData(id)
  // ...
}
app/item/[id]/page.tsx
import { getData } from './utils'

export const revalidate = 60

export default async function Page({
  params: { id },
}: {
  params: { id: string }
}) {
 const data = await getData(id)

  return <ItemPage data={data} />
}

unstable_cache

これが今回求めていた機能ですが、公式の記載でもある通り、まだ開発中のようです。
https://nextjs.org/docs/app/building-your-application/caching#unstable_cache

使い方は以下のような感じかなと思います。
実際使ってみたところ、ローカル上でデバッグした時はちゃんと動いていそでしたが、Vercelにデプロイすると、ビルド時以外は実行されず、ただのSSRになりました。

import { unstable_cache } from 'next/cache'

export default async function Page() {
  const data = await unstable_cache(
    async () => {
      const data = await db.query('...')
      return data
    },
    ['cache-key'],
    {
      tags: ['a', 'b', 'c'],
      revalidate: 10,
    }
  )()
  return <Home data={data} />
}

いろいろ試したときのサイトです。
https://isr-test-bay.vercel.app/

あとがき

このあたりがまとまった記事がなく、自分はこの部分でだいぶ時間を使ってしまったので、誰かの一助になればと思います。

補足

ISRをするときはnext/linkのprefetchにfalseを設定したほうがよさそうです。
指定していないと、ページが表示された際、Linkで指定されているページがrevalidateで指定した期間は関係なく再検証されます。

以下の例だと、Homeのページが表示された際、pageA、pageBも再検証されます。

import Link from "next/link";

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Link href='/pageA'>
       pageA
      </Link>
      <br />
      <Link href='/pageB'>
       pageB
      </Link>
    </main>
  )
}

以下のように、prefetch={false}を指定すると、リンク先のページは再検証されません。

import Link from "next/link";

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Link href='/pageA' prefetch={false}>
       pageA
      </Link>
      <br />
      <Link href='/pageB' prefetch={false}>
       pageB
      </Link>
    </main>
  )
}
GitHubで編集を提案

Discussion