0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ぼっちアドベントカレンダー by bon10Advent Calendar 2024

Day 20

PageRouterのキャッシュはswr、App RouterではrevalidatePathとrevalidateTagという選択

Posted at

Next.jsでCSR/SSRのアプリケーションを作っていると、APIのキャッシュ戦略も必要となる場面があります。
このときの手段としては以下のようなものがあるかと。

  • 一覧データをRecoilやJotaiなどの状態管理ライブラリを使って実現する
  • バックエンド側でキャッシュしてしまう(CDN、HTTP キャッシュ、Redisなど)
  • fetchやライブラリのキャッシュ実装を使う

フロントエンドよくわからない私からすると、状態管理ライブラリを使うとコンポーネントと責務を分割できるとはいえ、接合点がごちゃごちゃになってライブラリの依存性が高くなりがちだと思っています。(単に複雑になりやすいからわから使いこなせない)
なので、バックエンドでなるべくキャッシュしてしまって、フロントではAPIの実行可否にかかわらず解決することを先に考えると思います。
ただし、素の fetch で頑張るとなると、今度はフロントエンド側も少し実装を工夫する必要が出てきます。

かといってバックエンドにキャッシュ機構を作ろうと思ってもやっぱりちょっと面倒なので、中間対策としてキャッシュをいい感じに扱えるライブラリがあればいいなというのが初回の選択になりがちな気がしています。

そこでNext.jsのキャッシュ管理として筆頭に上がりそうなのが SWR です。

実装面でもあまり深くキャッシュについて考えなくてもよくて、とりあえずキー(APIのエンドポイント)さえ管理できていれば他の画面からも適当に mutate(エンドポイント) してしまえば勝手に更新してくれるお手軽さがあります。
自分でもよくわからなかったので動作のわかりやすいNext14の簡単なアプリを作って検証してみました。

まず素のNext.jsでPage Routerのアプリを作ります。選択肢は Would you like to use App Router? (recommended) のみ必ず NO を選択してください。それ以外はなんでもOKです。

npx create-next-app@latest
✔ What is your project named? … my-app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … No
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
Creating a new Next.js app in /xxxxx/my-app.

ディレクトリ直下で以下を実行し、swrを入れます。yarnを使ってますが、npmでもpnpmでも大丈夫です。

yarn add swr

次に、適当にHeaderコンポーネントを追加してみましょう。ChatGPTにお願いして生成してもらいつつ useSWRImmutable を追加してみました。

src/pages/components/Header.tsx
import Link from 'next/link';
import Image from 'next/image';
import useSWRImmutable from 'swr/immutable';

const Header = () => {
  const { data, error } = useSWRImmutable('/api/user/avatar', fetcher);
  console.log('render Header');

  return (
    <header className="w-full py-4 px-6 bg-white shadow-md">
      <nav className="max-w-7xl mx-auto flex justify-between items-center">
        <Link href="/" className="text-xl font-bold">
          My App
        </Link>
        <div className="flex gap-6">
          <Link href="/" className="hover:text-gray-600">
            Home
          </Link>
          <Link href="/about" className="hover:text-gray-600">
            About
          </Link>
          <Link href="/contact" className="hover:text-gray-600">
            Contact
          </Link>
        </div>
        <div className='flex justify-end items-center'>
          {error ? (
            <Image src="/globe.svg" alt="Default Avatar" width={32} height={32} />
          ) : (
            <Image src={data ? data.avatarUrl : '/globe.svg'} alt="User Avatar" width={32} height={32} />
          )}
        </div>
      </nav>
    </header>
  );
};

const fetcher = (url: string) => fetch(url).then(res => res.json());

export default Header;

次にAPIを作ります。Next.jsのルール通り src/pages/api/user/avatar.ts というディレクトリ・ファイル構成にします。

src/pages/api/user/avatar.ts
import { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    // 外部サービスから仮の画像を2つほどランダムで取得
    const urls = [
      'https://image.lgtmoon.dev/316618',
      'https://image.lgtmoon.dev/260085',
      'https://image.lgtmoon.dev/290299',
      'https://image.lgtmoon.dev/199372'
    ]
    const random = Math.floor(Math.random() * urls.length);
    const avatarUrl = urls[random];

    res.status(200).json({ avatarUrl });
  }

  if (req.method === 'POST') {
    // アバターを更新する処理。検証用なのでステータス200を返すだけ
    return res.status(200).json({ success: true });
  }

  // サポートされていないメソッド
  res.status(405).json({ error: 'Method not allowed' });
}

画像はローカルのでも良かったんですが、外部サービスとして私がよくPRにLGTMをするために使っている LGTMoon を使いました。

useSWRImmutable はSWRが提供しているもので、キャッシュを無効にするためのエイリアスみたいなものです。

これにより、リロード以外では画像が更新されなくなります。

次に index.tsx に、画像を更新する仕組みを仮実装します。

src/pages/index.tsx
export default function Home() {

  // Update Avatarボタン押下時の処理
  const handleChangeAvatar = async () => {
    console.log('change avatar');
    const response = await fetch('/api/user/avatar', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ avatarUrl: '/globe.svg' }),
    });
    if(!response.ok) {
      console.error('Failed to update avatar');
      return;
    }
    //const updatedData = await response.json();
    mutate('/api/user/avatar');
  }

  return (
    <div
      className={`${geistSans.variable} ${geistMono.variable} grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]`}
    >
    <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
    ()
      <button
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
          onClick={handleChangeAvatar}
        >
        Update Avatar
      </button>
    ()
    </main>
    ()

ボタンを押したら handleChangeAvatar が実行されるだけの簡単な実装です。
mutate('/api/user/avatar'); をするだけで、キー(URL)と一致するSWRのデータを更新してくれます。これは useSWRImmutable でも useSWR でも同じです。

ということでこのボタンを押下すれば、Headerコンポーネントの画像が切り替わることを確認することができます。めちゃくちゃ便利ですね。

App Routerの場合

Page Routerの実装サンプルで、SWRのキャッシュは、キーさえ一致すれば任意のタイミングで更新できることを説明しました。
例えばRecoilのように一意のキー名をAPIのエンドポイントに設定してどこかしらで管理さえしていれば、任意のタイミングでピンポイントに、かつ一部のデータのみキャッシュの更新が可能ということです。

App Routerの場合、revalidatePathとrevalidateTag があり、SWRのキャッシュとしては後者の revalidateTag が代替手段になりそうです。

たとえばPage Routerでヘッダーに表示する画像の取得API(GET: /api/user/avatar)にタグを付けるには以下のようにします。SWR→fetchに変えてます。

await fetch("/api/user/avatar", {
  next: { tags: ["avatar"] },
});

あとは index.tsxmutate を呼んでいた部分を変更するだけです。

const handleChangeAvatar = async () => {
    console.log('change avatar');
    const response = await fetch('/api/user/avatar', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ avatarUrl: '/globe.svg' }),
    });
    if(!response.ok) {
      console.error('Failed to update avatar');
      return;
    }
    //const updatedData = await response.json();
-    mutate('/api/user/avatar');
+    revalidateTag('avatar');
  }

めちゃくちゃシンプルです。フロントエンド初心者にはかなり理解しやすいですね。

参考:

ただ、キーを適当に量産したりすると意図せず更新したりされなかったり、同じドメインや要素なのに片方は更新されるが片方はされないなどの不都合が起きそうです。ここはルールで縛る、独自のLinterを作るなどして整合性を保つ必要がありそう。

まとめ

  • App RouterではSWRいらなくなりそう
  • revalidatePathもrevalidateTagも便利だが使い方にある程度ルールを作っておかないと複雑化しそう

Next.jsもReactも、知らない機能がまだまだありそうなので日々勉強です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?