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
は、サーバコンポーネント でのみ使用できます。
使用法
高コストな計算をキャッシュする
重複する処理をスキップするために 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} />
</>
);
}