cache
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
は呼び出されません。
与えられた引数で cachedFn
を呼び出すと、まずキャッシュにキャッシュされた結果が存在するかどうかを確認します。キャッシュされた結果が存在する場合、その結果を返します。存在しない場合、引数を使って fn
を呼び出し、結果をキャッシュに保存し、その結果を返します。fn
が呼び出されるのはキャッシュミスが発生したときだけです。
注意点
- 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
オブジェクトが Profile
と TeamReport
の両方でレンダーされる場合、2つのコンポーネントは作業を共有し、その user
に対して calculateUserMetrics
を一度だけ呼び出すことができます。
まず Profile
がレンダーされると仮定します。それは getUserMetrics
を呼び出し、キャッシュされた結果があるかどうかを確認します。その user
で getUserMetrics
が初めて呼び出されるので、キャッシュミスが発生します。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 はその作業の状態(pending、fulfilled、failed)とその最終的な結果を保持します。
この例では、非同期関数 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
は JavaScript の演算子で、Promise の結果を待って返します。最初の getData
呼び出しは単に fetch
を開始して Promise をキャッシュし、2回目の getData
がルックアップします。
2回目の呼び出し までに Promise がまだ pending の場合、await
は結果を待ちます。最適化は、fetch
を待っている間に React が計算作業を続けることができるため、2回目の呼び出し の待ち時間を短縮することです。
Promise がすでに解決している場合、エラーまたは fulfilled の結果になると、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
を使用して書き直すと、この場合 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} />
</>
);
}