cache - This feature is available in the latest Canary

Reactサーバーコンポーネント

cache は、React Server Componentsでのみ使用できます。

cache は、データフェッチや計算の結果をキャッシュできます。

const cachedFn = cache(fn);

リファレンス

cache(fn)

キャッシュ付きの関数バージョンを作成するには、コンポーネントの外でcacheを呼び出します。

import {cache} from 'react';
import calculateMetrics from 'lib/metrics';

const getMetrics = cache(calculateMetrics);

function Chart({data}) {
const report = getMetrics(data);
// ...
}

getMetrics が最初に data で呼び出されると、getMetricscalculateMetrics(data) を呼び出し、その結果をキャッシュに保存します。getMetrics が同じ data で再度呼び出された場合、calculateMetrics(data) を再度呼び出す代わりに、キャッシュされた結果を返します。

以下の例も参照してください。

パラメータ

  • fn: 結果をキャッシュしたい関数。fn は任意の引数を受け取り、任意の値を返すことができます。

戻り値

cache は、fn と同じ型シグネチャを持つ、キャッシュされたバージョンの fn を返します。その過程で fn を呼び出すことはありません。

特定の引数で cachedFn を呼び出すと、最初にキャッシュ内にキャッシュされた結果が存在するかどうかを確認します。キャッシュされた結果が存在する場合、その結果を返します。そうでない場合は、引数付きで fn を呼び出し、その結果をキャッシュに保存し、その結果を返します。fn が呼び出されるのは、キャッシュミスが発生した場合のみです。

注意

入力に基づいて戻り値をキャッシュする最適化は、メモ化として知られています。cacheから返された関数をメモ化された関数と呼びます。

注意点

  • React は、サーバーリクエストごとにすべてのメモ化された関数のキャッシュを無効にします。
  • cache を呼び出すたびに新しい関数が作成されます。これは、同じ関数で cache を複数回呼び出すと、同じキャッシュを共有しない異なるメモ化された関数が返されることを意味します。
  • cachedFn はエラーもキャッシュします。特定の引数に対して fn がエラーをスローした場合、そのエラーはキャッシュされ、cachedFn が同じ引数で呼び出されたときに、同じエラーが再度スローされます。
  • cache は、サーバーコンポーネントでのみ使用するためのものです。

使い方

コストのかかる計算をキャッシュする

cache を使用して、重複する作業をスキップします。

import {cache} from 'react';
import calculateUserMetrics from 'lib/user';

const getUserMetrics = cache(calculateUserMetrics);

function Profile({user}) {
const metrics = getUserMetrics(user);
// ...
}

function TeamReport({users}) {
for (let user in users) {
const metrics = getUserMetrics(user);
// ...
}
// ...
}

同じ user オブジェクトが ProfileTeamReport の両方でレンダリングされる場合、2つのコンポーネントは作業を共有し、その user に対して calculateUserMetrics を一度だけ呼び出すことができます。

Profile が最初にレンダリングされると仮定します。 getUserMetrics を呼び出し、キャッシュされた結果があるかどうかを確認します。 getUserMetrics がその user で呼び出されるのが初めてであるため、キャッシュミスが発生します。次に、getUserMetrics は、その usercalculateUserMetrics を呼び出し、結果をキャッシュに書き込みます。

TeamReportusers のリストをレンダリングし、同じ user オブジェクトに到達すると、getUserMetrics を呼び出して、キャッシュから結果を読み取ります。

落とし穴

異なるメモ化された関数を呼び出すと、異なるキャッシュから読み取られます。

同じキャッシュにアクセスするには、コンポーネントは同じメモ化された関数を呼び出す必要があります。

// Temperature.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export function Temperature({cityData}) {
// 🚩 Wrong: Calling `cache` in component creates new `getWeekReport` for each render
const getWeekReport = cache(calculateWeekReport);
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 Wrong: `getWeekReport` is only accessible for `Precipitation` component.
const getWeekReport = cache(calculateWeekReport);

export function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

上記の例では、PrecipitationTemperature はそれぞれ cache を呼び出して、独自のキャッシュルックアップを持つ新しいメモ化された関数を作成します。両方のコンポーネントが同じ cityData でレンダリングされる場合、calculateWeekReport を呼び出すために重複した作業を実行します。

さらに、Temperature は、コンポーネントがレンダリングされるたびに 新しいメモ化された関数を作成するため、キャッシュを共有できません。

キャッシュヒットを最大化し、作業を減らすには、2つのコンポーネントが同じメモ化された関数を呼び出して、同じキャッシュにアクセスする必要があります。代わりに、コンポーネント間で import できる専用のモジュールでメモ化された関数を定義します。

// getWeekReport.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export default cache(calculateWeekReport);
// Temperature.js
import getWeekReport from './getWeekReport';

export default function Temperature({cityData}) {
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import getWeekReport from './getWeekReport';

export default function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

ここでは、両方のコンポーネントが ./getWeekReport.js からエクスポートされた 同じメモ化された関数を呼び出して、同じキャッシュを読み書きします。

データのスナップショットを共有する

コンポーネント間でデータのスナップショットを共有するには、fetch のようなデータ取得関数で cache を呼び出します。複数のコンポーネントが同じデータフェッチを行うと、リクエストは1回だけ行われ、返されたデータはキャッシュされ、コンポーネント間で共有されます。すべてのコンポーネントは、サーバーレンダリング全体で同じデータのスナップショットを参照します。

import {cache} from 'react';
import {fetchTemperature} from './api.js';

const getTemperature = cache(async (city) => {
return await fetchTemperature(city);
});

async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

async function MinimalWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

AnimatedWeatherCardMinimalWeatherCard の両方が同じ city に対してレンダリングされる場合、メモ化された関数から同じデータのスナップショットを受け取ります。

AnimatedWeatherCardMinimalWeatherCard が、city 引数に異なる値を指定して getTemperature を呼び出した場合、fetchTemperature が2回呼び出され、それぞれの呼び出し箇所で異なるデータを受け取ります。

city はキャッシュキーとして機能します。

注意

非同期レンダリングは、Server Componentsでのみサポートされています。

async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

データのプリロード

時間のかかるデータフェッチをキャッシュすることで、コンポーネントのレンダリング前に非同期処理を開始できます。

const getUser = cache(async (id) => {
return await db.user.query(id);
});

async function Profile({id}) {
const user = await getUser(id);
return (
<section>
<img src={user.profilePic} />
<h2>{user.name}</h2>
</section>
);
}

function Page({id}) {
// ✅ Good: start fetching the user data
getUser(id);
// ... some computational work
return (
<>
<Profile id={id} />
</>
);
}

Page をレンダリングする際、コンポーネントは getUser を呼び出しますが、返されたデータは使用しないことに注意してください。この初期の getUser の呼び出しは、Page が他の計算処理や子要素のレンダリングを行っている間に発生する非同期データベースクエリを開始します。

Profile をレンダリングする際、再度 getUser を呼び出します。最初の getUser の呼び出しがすでに完了し、ユーザーデータがキャッシュされている場合、Profileこのデータを要求して待機するとき、別のリモートプロシージャコールを必要とせずにキャッシュから読み込むことができます。もし最初のデータリクエストが完了していない場合、このパターンのデータのプリロードはデータフェッチの遅延を軽減します。

詳細解説

非同期処理のキャッシュ

非同期関数を評価すると、その処理に対するPromiseを受け取ります。このPromiseはその処理の状態(保留成功失敗)とその最終的な確定結果を保持します。

この例では、非同期関数 fetchDatafetch を待機しているPromiseを返します。

async function fetchData() {
return await fetch(`https://...`);
}

const getData = cache(fetchData);

async function MyComponent() {
getData();
// ... some computational work
await getData();
// ...
}

getData を最初に呼び出すと、fetchData から返されたPromiseがキャッシュされます。それ以降の参照では、同じPromiseが返されます。

最初の getData の呼び出しは await しませんが、2回目await します。await は、Promiseの確定した結果を待機して返すJavaScript演算子です。最初の getData の呼び出しは、2回目の getData が参照できるようにPromiseをキャッシュするために、fetch を開始するだけです。

2回目の呼び出しまでにPromiseがまだ保留中の場合、await は結果を待機します。最適化されている点は、fetch を待機している間、Reactは計算処理を続行できるため、2回目の呼び出しの待ち時間が短縮されることです。

もしPromiseがすでにエラーまたは成功の結果に確定している場合、await はその値を即座に返します。どちらの結果でもパフォーマンス上のメリットがあります。

落とし穴

メモ化された関数をコンポーネント外で呼び出すと、キャッシュは使用されません。
import {cache} from 'react';

const getUser = cache(async (userId) => {
return await db.user.query(userId);
});

// 🚩 Wrong: Calling memoized function outside of component will not memoize.
getUser('demo-id');

async function DemoProfile() {
// ✅ Good: `getUser` will memoize.
const user = await getUser('demo-id');
return <Profile user={user} />;
}

React は、コンポーネント内のメモ化された関数に対してのみキャッシュアクセスを提供します。コンポーネントの外でgetUserを呼び出すと、関数は評価されますが、キャッシュの読み取りや更新は行われません。

これは、キャッシュアクセスがコンテキストを通して提供され、コンポーネントからのみアクセス可能であるためです。

詳細解説

cachememo、またはuseMemoはいつ使用すべきでしょうか?

言及されたすべての API はメモ化を提供しますが、違いは、何をメモ化することを意図しているか、誰がキャッシュにアクセスできるか、いつキャッシュが無効になるかです。

useMemo

一般に、クライアントコンポーネントでのレンダリング間でコストのかかる計算をキャッシュする場合は、useMemoを使用する必要があります。例として、コンポーネント内のデータの変換をメモ化する場合です。

'use client';

function WeatherReport({record}) {
const avgTemp = useMemo(() => calculateAvg(record), record);
// ...
}

function App() {
const record = getRecord();
return (
<>
<WeatherReport record={record} />
<WeatherReport record={record} />
</>
);
}

この例では、Appは同じレコードを持つ2つのWeatherReportをレンダリングします。両方のコンポーネントが同じ作業を行っていても、作業を共有することはできません。useMemoのキャッシュは、コンポーネントに対してローカルなだけです。

ただし、useMemoは、Appが再レンダリングされ、recordオブジェクトが変更されない場合、各コンポーネントインスタンスが作業をスキップし、avgTempのメモ化された値を使用することを保証します。useMemoは、特定の依存関係を持つavgTempの最後の計算のみをキャッシュします。

cache

一般に、サーバーコンポーネントで、コンポーネント間で共有できる作業をメモ化するには、cacheを使用する必要があります。

const cachedFetchReport = cache(fetchReport);

function WeatherReport({city}) {
const report = cachedFetchReport(city);
// ...
}

function App() {
const city = "Los Angeles";
return (
<>
<WeatherReport city={city} />
<WeatherReport city={city} />
</>
);
}

前の例をcacheを使用するように書き換えると、この場合、WeatherReportの2番目のインスタンスは、重複した作業をスキップして、最初のWeatherReportと同じキャッシュから読み取ることができます。前の例とのもう1つの違いは、cacheデータのフェッチをメモ化するためにも推奨されることです。useMemoは計算にのみ使用する必要があるのとは異なります。

現時点では、cacheはサーバーコンポーネントでのみ使用する必要があり、キャッシュはサーバーリクエスト間で無効になります。

memo

propsが変更されていない場合にコンポーネントの再レンダリングを防ぐには、memoを使用する必要があります。

'use client';

function WeatherReport({record}) {
const avgTemp = calculateAvg(record);
// ...
}

const MemoWeatherReport = memo(WeatherReport);

function App() {
const record = getRecord();
return (
<>
<MemoWeatherReport record={record} />
<MemoWeatherReport record={record} />
</>
);
}

この例では、両方のMemoWeatherReportコンポーネントが、最初にレンダリングされたときにcalculateAvgを呼び出します。ただし、Appが再レンダリングされ、recordに変更がない場合、propsのいずれも変更されておらず、MemoWeatherReportは再レンダリングされません。

useMemoと比較して、memoは、特定の計算ではなく、propsに基づいてコンポーネントのレンダリングをメモ化します。useMemoと同様に、メモ化されたコンポーネントは、最後のprop値を使用した最後のレンダリングのみをキャッシュします。propsが変更されると、キャッシュが無効になり、コンポーネントが再レンダリングされます。


トラブルシューティング

メモ化された関数が、同じ引数で呼び出しているにも関わらず、まだ実行される

前述の落とし穴を参照してください

上記に当てはまらない場合は、Reactがキャッシュに何かが存在するかどうかをチェックする方法に問題がある可能性があります。

引数がプリミティブ(オブジェクト、関数、配列など)でない場合は、同じオブジェクト参照を渡していることを確認してください。

メモ化された関数を呼び出すと、Reactは入力引数を調べて、結果が既にキャッシュされているかどうかを確認します。 Reactは、引数の浅い等価性を使用して、キャッシュヒットがあるかどうかを判断します。

import {cache} from 'react';

const calculateNorm = cache((vector) => {
// ...
});

function MapMarker(props) {
// 🚩 Wrong: props is an object that changes every render.
const length = calculateNorm(props);
// ...
}

function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}

この場合、2つのMapMarkerは同じ処理をしており、{x: 10, y: 10, z:10}という同じ値でcalculateNormを呼び出しているように見えます。オブジェクトには同じ値が含まれていますが、各コンポーネントが独自のpropsオブジェクトを作成するため、同じオブジェクト参照ではありません。

Reactは、キャッシュヒットがあるかどうかを確認するために、入力に対してObject.isを呼び出します。

import {cache} from 'react';

const calculateNorm = cache((x, y, z) => {
// ...
});

function MapMarker(props) {
// ✅ Good: Pass primitives to memoized function
const length = calculateNorm(props.x, props.y, props.z);
// ...
}

function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}

これに対処する1つの方法は、ベクトルの次元をcalculateNormに渡すことです。次元自体はプリミティブであるため、これでうまくいきます。

別の解決策は、ベクトルオブジェクト自体をコンポーネントへのpropsとして渡すことです。両方のコンポーネントインスタンスに同じオブジェクトを渡す必要があります。

import {cache} from 'react';

const calculateNorm = cache((vector) => {
// ...
});

function MapMarker(props) {
// ✅ Good: Pass the same `vector` object
const length = calculateNorm(props.vector);
// ...
}

function App() {
const vector = [10, 10, 10];
return (
<>
<MapMarker vector={vector} />
<MapMarker vector={vector} />
</>
);
}