Open16

Server Actions と revalidate の関係を追う

mugimugi

Server Actions の実行時、revalidateTag を呼んだときだけ再描画が走る。
これについて、裏側で何が起きてるのかを知りたい。

mugimugi

とりあえず動作の実験。

  • APIからは呼び出すたびに日時を返す
  • APIはrevalidate:0にしてキャッシュ無効で呼ぶ
  • Server Actions では特に何もしない
export default async function Page() {
  async function action() {
    "use server";

    // do nothing
  }

  const res = await fetch("http://localhost:3000/api", {
    next: { tags: ["mytag"], revalidate: 0 },
  });
  const { date } = await res.json();

  return (
    <form action={action}>
      <p>{date}</p>
      <button type="submit">Submit</button>
    </form>
  );
}

結果→ServerActionsを実行しても何も起きない

mugimugi

今度は revalidateTag を呼んでみる

import { revalidateTag } from "next/cache";

export default async function Page() {
  async function action() {
    "use server";

    revalidateTag("mytag");
  }

  const res = await fetch("http://localhost:3000/api", {
    next: { tags: ["mytag"], revalidate: 0 },
  });
  const { date } = await res.json();

  return (
    <form action={action}>
      <p>{date}</p>
      <button type="submit">Submit</button>
    </form>
  );
}

結果 → Server Actions を呼ぶたびに再描画される

mugimugi

レスポンスにも違いがある。
※Firefoxのdevtoolsで見れる

revalidateTagなし

revalidateTagあり

mugimugi
mugimugi

pathWasRevalidated を見てる箇所を探すと、handleAction() の中に行き着く

https://github.com/vercel/next.js/blob/c56915842a41ce5cd1328b36dff44004352c9f15/packages/next/src/server/app-render/action-handler.ts#L426-L437

handleAction() は Server Actions を処理してる関数
https://zenn.dev/cybozu_frontend/articles/server-actions-deep-dive#handleaction()

!staticGenerationStore.pathWasRevalidated なので、skipFlight は 「revalidateを行っていなかったら」trueになる。

mugimugi
mugimugi

walkTreeWithFlightRouterState はRSCのコンポーネントツリーの描画を行う部分。
options?.skipFlight が true の場合は、これを実行せずにnullにしてることがわかる。

つまり、revalidateTag を呼ぶと skipFlight は false になるので描画を行い、レスポンス(ここでいう buildIdFlightDataPair)に 設定される。

mugimugi

このとき、タグの値が何かはまったく関係が無い。
「revalidateTagを実行した」という時点で pathWasRevalidated に値は入る。

つまり、最初のサンプルコードで次のように全然関係ないタグをrevalidateしてみると...

import { revalidateTag } from "next/cache";

export default async function Page() {
  async function action() {
    "use server";

    revalidateTag("ahhhhhh"); // 全然関係ないタグ
  }

  const res = await fetch("http://localhost:3000/api", {
    next: { tags: ["mytag"], revalidate: 0 },
  });
  const { date } = await res.json();

  return (
    <form action={action}>
      <p>{date}</p>
      <button type="submit">Submit</button>
    </form>
  );
}

ふつうに再描画されるのがわかる。

mugimugi

結論

  • revalidateTag (revalidatePath) を呼ぶと、ページの再描画が実行されレスポンスに含まれる
  • タグやパスが現在のページと関連しているかは関係が無い

なおこれは2023/07/13現在でのcanaryバージョンでのコードをベースにした確認。
将来的に挙動が変わる可能性もある。

mugimugi

正式版の記事にしようかと思ったけど、α機能かつTODOコメントもある状態ということもあり、内容がわりと短期間で信頼できないものになりそうなので、このスクラップに留めておく