[fetch] Next.js version15の破壊的変更を確認してみる

## はじめに
Next.js version 15が2024.10.21にリリースされました
このリリースではキャッシュ周りの破壊的変更が行われたので調査してみました。

https://nextjs.org/blog/next-15-rc#caching-updates


## 前提知識
Next.jsのfetch apiがカスタムされていることへの理解
SSR, SSGの違いの理解


## 内容
今回注目したのはfetch apiのキャッシュ周りです。
version 14まではデフォルトでキャッシュされる仕様でした
version 15からはキャッシュ`されなく`なりました

参考にoptionを貼っておきます
```ts
fetch("url", { cache: "no-store" }) // キャッシュされない
fetch("url", { cache: "forcee-cache" }) // キャッシュされる
fetch("url", { next: { revalidate: 3600 } }) // 指定の時間でキャッシュを更新
```

---


公式ドキュメントを覗いてみると以下の内容を見つけました(日本語訳)
> このコンポーネントはブログ投稿のリストを取得して表示します。 fetchからの応答はデフォルトではキャッシュされません。 

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching

## APIを用意して実験してみる

ExpressでAPIを用意
これを叩いて実験してみます。
```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([{ id: 1, name: "tee" }]);
});
```

next.js version15
```ts
type User = {
  id: number;
  name: string;
};

export default async function Home() {
  const res = await fetch("http://localhost:3001");
  const data = await res.json();

  return (
      <div>
        {data.map((user: User) => (
          <div key={user.id}>
            <p>名前: {user.name}</p>
          </div>
        ))}
      </div>
  );
}
```

```
$ npm run build
$ npm start
```

![スクリーンショット 2025-02-02 10.57.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/567af0d2-9112-dec2-86e8-51962cc0c770.png)

SSGになりました。
表示を確認してみます。

![スクリーンショット 2025-02-02 10.59.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/97b0e9ff-cdb8-e6af-06a3-5ebfa0f8c655.png)

fetch apiはデフォルトでキャッシュしなくなったので、serverの内容を変えて、リロードすれば新しいデータが表示されるはずです。

```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([
  { id: 1, name: "tee" },
  { id: 1, name: "ren" }
  ]);
});
```
リロードしてみます

![スクリーンショット 2025-02-02 10.59.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/97b0e9ff-cdb8-e6af-06a3-5ebfa0f8c655.png)

変わりません...

## 原因を考えてみる
先ほどbuildした際はレンダリング方式がprerendered as static content(SSG)でした。
つまりbuild時にfetchが行われて、事前にHTMLの内容が作成された状態になります。
ユーザーがページに訪れた際はfetchのキャッシュがどうとか関係なくなります。 実行されないですからね...

公式ドキュメントを確認して、以下の内容が見つかりました。

> このルートの他の場所で動的 API を使用していない場合は、静的ページに事前レンダリングされます。その後、増分静的再生成をnext build使用してデータを更新できます。


https://nextjs.org/docs/app/building-your-application/data-fetching/fetching

つまり、ISRにするか、SSRのような動的生成にする必要があります。


## 状況を整理して実験してみる
next.js version 15ではfetch apiはデフォルトでキャッシュ`されない`
next.js version 14ではfetch apiはデフォルトでキャッシュ`される`

SSG(ISRなし)だとfetch　apiのキャッシュ云々が関係なくなる

no-storeのfetchを1つ増やすことでSSRにしてみます。

---
apiを追加
```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([{ id: 1, name: "tee" }]);
});

app.get("/posts", (req: Request, res: Response) => {
  res.status(200).json([{ id: 1, name: "post1" }]);
});
```

no-storeのfetchを追加する
```ts
type User = {
  id: number;
  name: string;
};

type Post = User;

export default async function Home() {
  const res = await fetch("http://localhost:3001");
  const data = await res.json();

  const res2 = await fetch("http://localhost:3001/posts", {
    cache: "no-store",
  });
  const data2 = await res2.json();

  return (
    <>
      <div>
        {data.map((user: User) => (
          <div key={user.id}>
            <p>名前: {user.name}</p>
          </div>
        ))}
      </div>
      <hr />
      <div>
        {data2.map((post: Post) => (
          <div key={post.id}>
            <p>名前: {post.name}</p>
          </div>
        ))}
      </div>
    </>
  );
}
```

```
$ npm run build
$ npm start
```
ssrになりました
![スクリーンショット 2025-02-02 11.21.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/62e555f2-8cf9-f8ef-f28e-46726a571b94.png)
表示
![スクリーンショット 2025-02-02 11.22.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/1f9460bb-47c9-7302-fa73-815fd8c5a956.png)

---


ここでサーバーを以下のように変更して、ページをリロードしてみます。
no-storeを指定している`/posts`はもちろん、デフォルトでキャッシュしない`/`も更新されるはずです。
```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([
    { id: 1, name: "tee" },
    { id: 2, name: "ren" },
  ]);
});

app.get("/posts", (req: Request, res: Response) => {
  res.status(200).json([
    { id: 1, name: "post1" },
    { id: 2, name: "post2" },
  ]);
});
```

ページをリロード
![スクリーンショット 2025-02-02 11.27.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/01375492-b478-9045-a59c-b6c022dfa07a.png)

予想通り更新されました。

## Next.js version14の時と比べる
以下コマンドでversionだけ落としたものを作成してみます。
```
$ npx create-next-app@14.2.23
```

apiを元に戻します
```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([{ id: 1, name: "tee" }]);
});

app.get("/posts", (req: Request, res: Response) => {
  res.status(200).json([{ id: 1, name: "post1" }]);
});
```
表示
![スクリーンショット 2025-02-02 11.33.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/9b4194af-09dd-6c4c-d839-b43e27c879e0.png)

ではサーバーを以下のように変更して、ページをリロードしてみます。
no-storeを指定している/postsは更新されるはずです。
next.js version 14ではデフォルトで`キャッシュする`はずなので、`/`は更新されないはずです。


```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([
    { id: 1, name: "tee" },
    { id: 2, name: "ren" },
  ]);
});

app.get("/posts", (req: Request, res: Response) => {
  res.status(200).json([
    { id: 1, name: "post1" },
    { id: 2, name: "post2" },
  ]);
});
```

![スクリーンショット 2025-02-02 11.37.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/bdb26a7f-c251-c6b6-8d8d-cdcfb27b7b47.png)

予想通り、`/`は更新されませんでした。

## 明示的にno-storeを指定した場合

APIは1つだけで、明示的にno-storeを指定してみます。
api
```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([{ id: 1, name: "tee" }]);
});
```

next.js version15
```ts
type User = {
  id: number;
  name: string;
};

export default async function Home() {
  const res = await fetch("http://localhost:3001", {
    cache: "no-store",
  });
  const data = await res.json();

  return (
    <div>
      {data.map((user: User) => (
        <div key={user.id}>
          <p>名前: {user.name}</p>
        </div>
      ))}
    </div>
  );
}
```

```
$ npm run build
$ npm start
```
ssrになりました
![スクリーンショット 2025-02-02 11.43.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/5789e1ee-d3b2-da85-61f6-a75b4a13c76d.png)

表示

![スクリーンショット 2025-02-02 11.44.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/495fbb9c-d95a-a9bf-8b13-5f1b6cf1117b.png)

---
apiを変更してリロード
```ts
app.get("/", (req: Request, res: Response) => {
  res.status(200).json([
  { id: 1, name: "tee" },
  { id: 2, name: "ren" }
  ]);
});
```

![スクリーンショット 2025-02-02 11.48.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/46c697c5-2b7b-4c9c-aa21-c2222e0444ee.png)

build時にSSRだったのは確認していたので予想通りですね。



## まとめ

Next.js version15ではfetch APIはデフォルトでキャッシュされない
他の場所で動的APIを使用していない場合SSGとして事前レンダリングされる
SSGの場合ページ訪問時にfetchが走らないので、キャッシュされているような挙動になる
no-storeを明示的に付与するとSSRとしてレンダリングされる

ちょっとパズルみたいですね。
単体でfetchを使う場合はSSGになってしまう可能性があるので気をつけるべし

**今回の反省**
fetchのキャッシュとレンダリング方式は別物ではありますが、fetchの方法によってある程度レンダリング方式が決まってきます。
以下のように覚えてしまうと、今回のような予期しない挙動に悩まされます。

```ts
fetch("url", { cache: "no-store" }) // SSR
fetch("url", { cache: "forcee-cache" }) // SSG
fetch("url", { next: { revalidate: 3600 } }) // ISR
```
~~そもそもこれじゃCSRを考慮できてない~~

正しくは以下 + 条件によってはレンダリング方式を意識する。ですかね
```ts
fetch("url") // デフォルト挙動はキャッシュされない
fetch("url", { cache: "no-store" }) // キャッシュされない
fetch("url", { cache: "forcee-cache" }) // キャッシュされる
fetch("url", { next: { revalidate: 3600 } }) // 指定の時間でキャッシュを更新
```
no-storeがデフォルト値として考えるのもちょっと微妙ですね
明示的に指定した場合レンダリング方式がSSRに決定されて、キャッシュしませんが、省略すると、条件によってはSSGにレンダリングされて、キャッシュされたような挙動になります。
no-storeを明示的に指定することで、SSGになるのを回避できるわけなので、
全てのfetchに明示的にno-storeをつけるのが一番事故が少ないかもしれません。


fetchはデフォルトでキャッシュしないが、気をつけないとSSGになってしまい、キャッシュされたような挙動を撮ってしまうというのは若干分かりにくような気もします。
今回fetchのキャッシュ周りの戦略が変わったように、将来的に変わる可能性もあるのかな〜という個人の感想です。


## 追記
公式ドキュメントのapi/function/fetchを見ると明確でした
デフォルト値はauto no cacheで、キャッシュされませんが、動的APIの使用が検出されない場合、build時に1回だけ実行されるとの説明がありまし。
やはりfetch("")のデフォルトはSSGである。SSRであるといった覚え方だとハマりそうですね。

fetchによるData cacheとSSGのようなfull route cacheは関連づけずに覚えるべきだと言うことを理解できました

![スクリーンショット 2025-02-02 13.43.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1390680/7c4a883f-2828-fbf4-d4c0-100199360ce6.png)


## 参考
https://nextjs.org/blog/next-15-rc

https://nextjs.org/docs/app/building-your-application/data-fetching/fetching

https://zenn.dev/akfm/articles/nextjs-cache-default-update