キャッシュThis feature is available in the latest Canary

Canary

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 は呼び出されません。

与えられた引数で cachedFn を呼び出すと、まずキャッシュにキャッシュされた結果が存在するかどうかを確認します。キャッシュされた結果が存在する場合、その結果を返します。存在しない場合、引数を使って fn を呼び出し、結果をキャッシュに保存し、その結果を返します。fn が呼び出されるのはキャッシュミスが発生したときだけです。

補足

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

注意点

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

使い方

高コストな計算をキャッシュする

重複する作業をスキップするために 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 を呼び出し、キャッシュされた結果があるかどうかを確認します。その usergetUserMetrics が初めて呼び出されるので、キャッシュミスが発生します。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-ed します。

// 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 はキャッシュキーとして機能します。

補足

非同期レンダリング はサーバーコンポーネントでのみサポートされています。

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 はその作業の状態(pendingfulfilledfailed)とその最終的な結果を保持します。

この例では、非同期関数 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 は JavaScript の演算子で、Promise の結果を待って返します。最初の getData 呼び出しは単に fetch を開始して Promise をキャッシュし、2回目の getData がルックアップします。

2回目の呼び出し までに Promise がまだ pending の場合、await は結果を待ちます。最適化は、fetch を待っている間に React が計算作業を続けることができるため、2回目の呼び出し の待ち時間を短縮することです。

Promise がすでに解決している場合、エラーまたは fulfilled の結果になると、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 を呼び出すと、関数は評価されますが、キャッシュは読み取られず、更新もされません。

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

さらに深く知る

cachememouseMemo のどれをいつ使うべきですか?

すべての言及された 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 のキャッシュはコンポーネントに対してのみローカルです。

しかし、useMemoApp が再レンダーされ、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 を使用して書き直すと、この場合 2 番目の WeatherReport インスタンス は重複する作業をスキップし、最初の WeatherReport と同じキャッシュから読み取ることができます。前の例とのもう一つの違いは、cacheデータフェッチのメモ化 にも推奨されていることで、これは useMemo が計算のみに使用すべきであるとは対照的です。

現時点では、cache はサーバーコンポーネントでのみ使用すべきで、キャッシュはサーバーリクエスト間で無効化されます。

memo

memo は、props が変更されない場合にコンポーネントの再レンダリングを防ぐために使用すべきです。

'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 に基づいてコンポーネントのレンダリングをメモ化します。これは特定の計算に対してではなく、メモ化されたコンポーネントは最後のレンダリングと最後の 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 は同じ作業を行い、calculateNorm{x: 10, y: 10, z:10} の同じ値で呼び出しているように見えます。オブジェクトが同じ値を含んでいても、それぞれのコンポーネントが自身の 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} />
</>
);
}

これを解決する一つの方法は、ベクトルの次元を 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} />
</>
);
}