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) を受け取ります。プロミスはその処理の状態 (pending、fulfilled、failed) とその最終的な結果を保持します。
この例では、非同期関数 fetchData
は fetch
結果を待機するプロミスを返します。
async function fetchData() {
return await fetch(`https://...`);
}
const getData = cache(fetchData);
async function MyComponent() {
getData();
// ... some computational work
await getData();
// ...
}
最初の getData
呼び出しでは、fetchData
から返されたプロミスがキャッシュされます。その後のキャッシュ探索では、同じプロミスが返されます。
最初の getData
呼び出しでは await
しておらず、2 回目の呼び出し では await
していることに注目してください。await
は JavaScript の演算子であり、プロミスの結果を待機して返します。最初の getData
呼び出しは単に fetch
を開始してプロミスをキャッシュし、2 回目の getData
のときに見つかるようにしているのです。
2 回目の呼び出し時点でプロミスがまだ pending の場合、await
は結果を待ちます。fetch
を待っている間に React が計算処理を続けることができるため、2 回目の呼び出しの待ち時間を短縮できる、という最適化になります。
プロミスの最終状態がすでに決定 (settled) している場合、結果がエラーの場合でも正常終了 (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 に基づいてコンポーネントのレンダーをメモ化します。一方で最後の props の値に対応する最後のレンダー結果だけがキャッシュされるという点では useMemo
と似ています。props が変更されるとキャッシュは無効化され、コンポーネントは再レンダーされます。
トラブルシューティング
メモ化された関数が、同じ引数で呼び出されても実行される
上で述べた落とし穴を参照してください。
上記のいずれも該当しない場合、何かがキャッシュに存在するかどうかを React がチェックする方法に関連した問題かもしれません。
引数がプリミティブでない場合(例:オブジェクト、関数、配列)、同じオブジェクト参照を渡していることを確認してください。
メモ化された関数を呼び出すとき、React は入力された引数を調べて結果がすでにキャッシュされているかどうかを確認します。React は引数に対して浅い (shallow) 比較を行い、キャッシュヒットがあるかどうかを判断します。
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} />
</>
);
}