【Next.js】unstable_cacheの代わりに推奨しているuse cacheについて

## はじめに
Next.jsのバージョン14では非同期関数の結果を`Data Cache`に保存する関数として[`unstable_cache`](https://nextjs.org/docs/app/api-reference/functions/unstable_cache)が用意されていました。

```ts
const data = unstable_cache(
  // データを取り出す非同期関数
  getData,
  // キャッシュを識別するキー
  ['data'],
  {
    // キャッシュをまとめて無効にするデータのタグ
    tags: ['user-data'],
    // 与えられた秒数が経過した後のデータ取得後にデータを再検証する
    // 未定義かfalseだとrevalidateTagやrevalidatePathをしない限りデータを再検証しない
    revalidate: 3600,
  },
)();
```

Next.jsのバージョン15では`unstable_cache`が廃止され、`use cache`ディレクティブがその代わりを担うようになりました。
`use cache`はどのような働きをしてくれるのでしょうか。この記事では`use cache`の働きについて解説します。

## Data Cache
`Data Cache`はNext.jsが行う4つのキャッシュのうちの1つです。
他のキャッシュには、リクエスト単位に行われる`Request Memorization`、ビルド時に行われる`Full Route Cache`、クライアント側で行われる`Client-side Router Cache`があります。

`Data Cache`は異なるリクエストやデプロイ間で共有される永続的なキャッシュです。説明では`fetch`と`Data Cache`についての関係について考えます。後ほどここで紹介したものと`use cache`と`Data Cache`との関係が対応します。

以下のように`fetch`で`options.cache`を`force-cache`にしたデータ取得では、`Data Cache`にデータがあればそのまま返し、なければ新たにデータを取得して`Data Cache`への保存とリクエスト先への返却を行います。
```ts
fetch(url, { cache: 'force-cache' });
```
Next.js15からは`options.cache`を指定しない時の`fetch`は他の非同期関数と同じように振る舞います。後述しますが、`use cache`を使う世界では、`fetch`の`options.cache`によるキャッシュの管理は必要ないかも知れません。

### 再検証
キャッシュされたデータは2種類の方法でパージされます。

1つ目は`fetch`で`options.next.revalidate`を与えた時です。
```ts
fetch(url, { next: { revalidate: 60 } });
```
上記のように`options.next.revalidate`を60に設定した場合、最後にデータを取り出して60秒経過してから行われたデータ取得を最後にキャッシュが更新されます。
60秒経過するとキャッシュが自動的に削除されるのではなく、60秒経過後のデータの取得が終わったタイミングで`Data Cache`を更新することに注意が必要です([`stale-while-revalidate`](https://web.dev/articles/stale-while-revalidate)のような動きです)。

```ts
// 最初のデータ取得 データの返却+データをData Cacheに保存
fetch(url, { next: { revalidate: 60 } });

// 60秒以内のデータ取得 Data Cacheから結果を返す
fetch(url, { next: { revalidate: 60 } });

// さらに60秒以上経過してからデータ取得
// Data Cacheから結果が返される+Data Cacheを削除して新しいデータに更新
fetch(url, { next: { revalidate: 60 } });

// 1つ前のfetchで更新されたデータをData Cacheから返す
fetch(url, { next: { revalidate: 60 } });
```

2つ目は`revalidatePath`・`revalidateTag`を用いた時です。
`revalidatePath`は`revalidatePath('/dashboard')`のように引数に再検証させたいパスを渡して実行します。この操作は対象のパスで利用している`Data Cache`を全て削除します。次のリクエストでデータの取得と`Data Cache`へのキャッシュが行われます。
`revalidateTag`は`revalidateTag('user')`のように使います。引数に渡された文字列はタグで、そのタグを持つ`Data Cache`を全て削除します。`Data Cache`のタグは`fetch`の`options.next.tags`で渡せます。
```ts
fetch(url, { next: { tags: ['user', 'blog'] } });
```

### キャッシュしない
`fetch`の結果をキャッシュしたくない場合は`options.cache`を`no-store`にします。
```ts
fetch(url, { cache: 'no-store' });
```
このようにした場合、Next.jsは`Data Cache`を確認せずに常に新たに取得した結果を返しますし、その結果を`Data Cache`に保存しません。この場合も`Request Memorization`は行われることに注意してください。

`options.cache`がデフォルトの場合と`no-store`を指定した場合の違いはルートが動的になるかどうかにあります。`no-store`の場合は動的ルートになり常にデータを取得しますが、デフォルトの場合は他の要素によって動的ルートにならない限りは`Full Route Cache`を返すのでデータの取得を行いません。

## use cache
`fetch`と合わせて`Data Cache`がどのようなものかを紹介しました。
次に、`use cache`を用いた`Data Cache`の取り扱いについて紹介します。

:::note
Next.jsの最新のバージョンが15.1の時にこの記事を作成しました。
`use cache`はバージョン15.1にて`canary`とされています。動作の変更によってこの記事に誤った記述が生じることに注意してください。
:::

`use cache`を使う場合はまず、`next.config.ts`で`experimental.dynamicIO`を`true`にします。

```ts:next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  ...
  experimental: {
    ...
    dynamicIO: true,
  },
}
 
export default nextConfig
```

`dynamicIO`を`ture`にすると、`use cache`のスコープにないすべての非同期関数を利用する箇所が事前レンダリングから除外されます。パフォーマンスの低下に気をつけましょう。

`use cache`は`use server`と同じようにファイルの先頭やコンポーネント・関数の記述の先頭で宣言します。

`use cache`が反映されたスコープは、すべての非同期関数が`Data Cache`とやりとりします(手動でオプトアウトしない限り)。つまり、そのスコープの非同期関数はすべて`fetch`の`options.cache`に`force-cache`を付与したときと同じような動きをします。

`use cache`スコープ内で呼ばれた非同期関数は、その入力値等を元にキャッシュキーを自動で作成します。キャッシュキーを構成する値は[シリアライズ](https://ja.react.dev/reference/rsc/use-server#serializable-parameters-and-return-values)可能なものだけで、シリアライズできない値はキャッシュキーに含まれないです。キャッシュヒットについての不具合等につながるので注意が必要です。
また、非同期関数の戻り値はシリアライズされて`Data Cache`へ保存されるのでシリアライズ可能な値である必要があります。

`use cache`はファイルに対して、以下のように付与します。
```ts
'use cache';

export default async function Page() {
  const data = await getData();
  // ...
}
```
このように宣言したファイルにネストされた全てのコンポーネントは`use cache`を継承します。
`use cache`を付与されたファイルのコンポーネントは、すべてのデータのやり取りを`Data Cache`を通じることを仮定できるので`Suspense`なしで非同期コンポーネントを配置できます。

逆に`Suspense`は`use cache`の境界となり、`Suspense`内のコンポーネントは動的にデータ取得を行います(`children`も境界になります)。
```tsx
// without-suspense/page.tsx
'use cache';

export default async function PageWithoutSuspense() {
  return (
    <div>
      <UserData />
    </div>
  );
};

// with-suspense/page.tsx
'use cache';

export default async function PageWithSuspense() {
  return (
    <div>
      <Suspense fallback={<UserDataFallback />}>
        <UserData />
      </Suspense>
    </div>
  );
};

// user-data.tsx
export const UserData = () => {
  const user = await getUser();
  return ...
}
```
上記の例の`WithoutSuspensePage`では`UserData`のデータ取得で`Data Cache`へデータを問い合わせて取得するのに対して、`WithSuspensePage`は`UserData`のデータ取得で常に新しいデータを取得します。

コンポーネントは以下のように利用します。
```ts
export const CacheComponent = () => {
  'use cache';
  const data = await getData();

  return ...
};
```
そのコンポーネント内で実行されるすべての非同期関数の結果が`Data Cache`に保存されます。異なる場所からこのコンポーネントを再利用する場合も、生成されるキャッシュキーに違いがない場合は同じキャッシュが利用されます。

関数は以下のように利用します。
```ts
export const getData = async () => {
  'use cache';
  const data = await fetch(url);
  return data;
};
```
`fetch`であれば`fetch(url, { cache: 'force-cache' })`のようにし、それ以外であれば`unstable_cache`を使うようにする必要がなくなり、同じ`use cache`で管理できるようになりました。

### 再検証
`Data Cache`で再検証を行う方法は2つありました。`use cache`でもそれは同じです。

1つ目の再検証を行うまでの時間を定義する方法は`cacheLife`を利用します。
`cacheLife`は`fetch`の`options.next.revalidate`のように秒数を渡すのではなく、`seconds`、`minutes`、`hours`、`days`、`weeks`、`max`の決められた値を渡します。
それぞれの値は以下のように動きます。

| value | Stale | Revalidate | Expire |
| --- | --- | --- | --- |
| デフォルト | undefined | 15分 | 無限 |
| seconds | undefined | 1秒 | 1秒 |
| minutes | 5分 | 1分 | 1時間 |
| hours | 5分 | 1時間 | 1日 |
| days | 5分 | 1日 | 1週間 |
| weeks | 5分 | 1週間 | 1ヶ月 |
| max | 5分 | 1ヶ月 | 無限 |

`Stale`はクライアント側でデータを`Fresh`とみなす期間で、これを超えるとキャッシュを削除して次の実行からサーバーへデータの確認を行います。
`Revalidate`はサーバーがデータのキャッシュの期限が切れていないことを確認する期間です。
`Expire`はキャッシュが有効切れするまでの期間を指します。

2つ目の再検証である`revalidatePath`、`revalidateTag`で行う方法はそのまま使えます。
再検証の目印となるタグをは`cacheTag`で付与します。
```ts
const getUsers = async () => {
  'use cache';
  const users = await getUsers();
  cacheTag('users', 'user');
  for (const user of users) {
    cacheTag(`user-${user.id}`);
  }
  return users;
};
```
この場合`getUser`は`users`、`user`、`user-id`・・・がタグとして紐づけられます。

### キャッシュしない
非同期関数がなくても対象の場所で動的に実行させたい場合は`connection`を使います。
```ts
import { connection } from 'next/server'
 
export default async function Page() {
  await connection()
  ...
}
```
`connection`から先はすべてプレレンダリングから除外されます。これも`canary`の機能なので利用には気をつけましょう。

## おわりに
`Data Cache`と`use cache`について紹介しました。
Next.jsバージョン14の世界では`fetch`とその他の非同期関数でキャッシュする方法が異なり大変でしたが、`use cache`の出現により`fetch`を他の非同期関数と同じように扱えるのでそれが解決されとても便利になりました。

これまでの動的ルートと静的ルートで物事を考えていると概念が混ざってわかりにくくなるので、別のモデルとして考えた方が理解しやすそうだなと思いました。