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
で呼び出されると、getMetrics
は calculateMetrics(data)
を呼び出し、その結果をキャッシュに保存します。getMetrics
が同じ data
で再度呼び出された場合、calculateMetrics(data)
を再度呼び出す代わりに、キャッシュされた結果を返します。
パラメータ
fn
: 結果をキャッシュしたい関数。fn
は任意の引数を受け取り、任意の値を返すことができます。
戻り値
cache
は、fn
と同じ型シグネチャを持つ、キャッシュされたバージョンの fn
を返します。その過程で fn
を呼び出すことはありません。
特定の引数で cachedFn
を呼び出すと、最初にキャッシュ内にキャッシュされた結果が存在するかどうかを確認します。キャッシュされた結果が存在する場合、その結果を返します。そうでない場合は、引数付きで fn
を呼び出し、その結果をキャッシュに保存し、その結果を返します。fn
が呼び出されるのは、キャッシュミスが発生した場合のみです。
注意点
- 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
オブジェクトが Profile
と TeamReport
の両方でレンダリングされる場合、2つのコンポーネントは作業を共有し、その user
に対して calculateUserMetrics
を一度だけ呼び出すことができます。
Profile
が最初にレンダリングされると仮定します。 getUserMetrics
を呼び出し、キャッシュされた結果があるかどうかを確認します。 getUserMetrics
がその user
で呼び出されるのが初めてであるため、キャッシュミスが発生します。次に、getUserMetrics
は、その user
で calculateUserMetrics
を呼び出し、結果をキャッシュに書き込みます。
TeamReport
が users
のリストをレンダリングし、同じ user
オブジェクトに到達すると、getUserMetrics
を呼び出して、キャッシュから結果を読み取ります。
データのスナップショットを共有する
コンポーネント間でデータのスナップショットを共有するには、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);
// ...
}
AnimatedWeatherCard
と MinimalWeatherCard
の両方が同じ city に対してレンダリングされる場合、メモ化された関数から同じデータのスナップショットを受け取ります。
AnimatedWeatherCard
と MinimalWeatherCard
が、city 引数に異なる値を指定して getTemperature
を呼び出した場合、fetchTemperature
が2回呼び出され、それぞれの呼び出し箇所で異なるデータを受け取ります。
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はその処理の状態(保留、成功、失敗)とその最終的な確定結果を保持します。
この例では、非同期関数 fetchData
は fetch
を待機している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
はその値を即座に返します。どちらの結果でもパフォーマンス上のメリットがあります。
詳細解説
言及されたすべての 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} />
</>
);
}