cache
vous permet de mettre en cache le résultat d’un chargement de données ou d’un calcul.
const cachedFn = cache(fn);
Référence
cache(fn)
Appelez cache
hors de tout composant pour créer une variante d’une fonction dotée de mise en cache.
import {cache} from 'react';
import calculateMetrics from 'lib/metrics';
const getMetrics = cache(calculateMetrics);
function Chart({data}) {
const report = getMetrics(data);
// ...
}
Lors du premier appel de getMetrics
avec data
, getMetrics
appellera calculateMetrics(data)
et mettra le résultat en cache. Si getMetrics
est rappelée avec le même argument data
, elle renverra le résultat mis en cache plutôt que de rappeler calculateMetrics(data)
.
Voir d’autres exemples plus bas.
Paramètres
fn
: la fonction dont vous voulez mettre les résultats en cache.fn
peut prendre un nombre quelconque d’arguments et renvoyer n’importe quel type de résultat.
Valeur renvoyée
cache
renvoie une version de fn
dotée d’un cache, avec la même signature de type. Elle n’appelle pas fn
à ce moment-là.
Lors d’un appel à cachedFn
avec des arguments donnés, elle vérifiera d’abord si un résultat correspondant existe dans le cache. Si tel est le cas, elle renverra ce résultat. Dans le cas contraire, elle appellera fn
avec les arguments, mettra le résultat en cache et le renverra. fn
n’est appelée qu’en cas d’absence de correspondance dans le cache (cache miss, NdT).
Limitations
- React invalidera le cache de toutes les fonctions mémoïsées à chaque requête serveur.
- Chaque appel à
cache
crée une nouvelle fonction. Ça signifie qu’appelercache
plusieurs fois avec la même fonction renverra plusieurs fonctions mémoïsées distinctes, avec chacune leur propre cache. cachedFn
mettra également les erreurs en cache. Sifn
lève une exception pour certains arguments, ce sera mis en cache, et la même erreur sera levée lorsquecachedFn
sera rappelée avec ces mêmes arguments.cache
est destinée uniquement aux Composants Serveur.
Utilisation
Mettre en cache un calcul coûteux
Utilisez cache
pour éviter de dupliquer un traitement.
import {cache} from 'react';
import calculateUserMetrics from 'lib/user';
const getUserMetrics = cache(calculateUserMetrics);
function Profile({user}) {
const metrics = getUserMetrics(user);
// ...
}
function TeamReport({users}) {
for (const user of users) {
const metrics = getUserMetrics(user);
// ...
}
// ...
}
Si le même objet user
est affiché dans Profile
et TeamReport
, les deux composants peuvent mutualiser le travail et n’appeler calculateUserMetrics
qu’une fois pour ce user
.
Supposons que Profile
fasse son rendu en premier. Il appellera getUserMetrics
, qui vérifiera si un résultat existe en cache. Comme il s’agit du premier appel de getUserMetrics
pour ce user
, elle ne trouvera aucune correspondance. getUserMetrics
appellera alors effectivement calculateUserMetrics
avec ce user
puis mettra le résultat en cache.
Lorsque TeamReport
affichera sa liste de users
et atteindra le même objet user
, il appellera getUserMetrics
qui lira le résultat depuis son cache.
Partager un instantané de données
Pour partager un instantané de données d’un composant à l’autre, appelez cache
sur une fonction de chargement de données telle que fetch
. Lorsque plusieurs composants feront le même chargement de données, seule une requête sera faite, et ses données résultantes mises en cache et partagées à travers plusieurs composants. Tous les composants utiliseront le même instantané de ces données au sein du rendu côté serveur.
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);
// ...
}
Si AnimatedWeatherCard
et MinimalWeatherCard
s’affichent tous deux avec la même city, ils recevront le même instantané de données depuis la fonction mémoïsée.
Si AnimatedWeatherCard
et MinimalWeatherCard
fournissent un argument city différent à getTemperature
, alors fetchTemperature
sera appelée deux fois, et chaque point d’appel recevra ses données spécifiques.
La city agit comme une clé de cache.
Précharger des données
En mettant en cache un chargement de données qui prendrait du temps, vous pouvez démarrer des traitements asynchrones avant de faire le rendu d’un composant.
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}) {
// ✅ Malin : commence à charger les données utilisateur
getUser(id);
// ... des calculs ici
return (
<>
<Profile id={id} />
</>
);
}
Lorsque Page
fait son rendu, le composant appelle getUser
, mais remarquez qu’il n’utilise pas les données renvoyées. Cet appel anticipé à getUser
déclenche la requête asynchrone à la base de données, qui s’exécute pendant que Page
fait d’autres calculs puis déclenche le rendu de ses enfants.
Lorsque Profile
fait son rendu, nous appelons à nouveau getUser
. Si l’appel initial à getUser
a fini son chargement et mis en cache les données utilisateur, lorsque Profile
demande ces données puis attend, il n’a plus qu’à les lire du cache, sans relancer un appel réseau. Si la requête de données initiale n’est pas encore terminée, cette approche de préchargement réduit tout de même le délai d’obtention des données.
En détail
Lorsque vous évaluez une fonction asynchrone, vous recevez une Promise représentant le traitement. La promesse maintient un état pour le traitement (en attente, accompli ou rejeté) ainsi que l’aboutissement du traitement à terme.
Dans cet exemple, la fonction asynchrone fetchData
renvoie une promesse pour le résultat de notre appel à fetch
.
async function fetchData() {
return await fetch(`https://...`);
}
const getData = cache(fetchData);
async function MyComponent() {
getData();
// ... des calculs ici
await getData();
// ...
}
En appelant getData
pour la première fois, la promesse renvoyée par fetchData
est mise en cache. Les appels ultérieurs utiliseront la même promesse.
Remarquez que le premier appel à getData
n’appelle pas await
, alors que le second le fait. await
est un opérateur JavaScript qui attend l’établissement de la promesse et renvoie son résultat accompli (ou lève son erreur de rejet). Le premier appel à getData
lance simplement le chargement (fetch
) pour mettre la promesse en cache, afin que le deuxième getData
la trouve déjà en cours d’exécution.
Si lors du deuxième appel la promesse est toujours en attente, alors await
attendra son résultat. L’optimisation tient à ce que, pendant le fetch
issu du premier appel, React peut continuer son travail de calcul, ce qui réduit l’attente pour le deuxième appel.
Si la promesse est déjà établie à ce moment-là, await
renverra immédiatement la valeur accomplie (ou lèvera immédiatement l’erreur de rejet). Dans les deux cas, on améliore la performance perçue.
En détail
Toutes ces API proposent de la mémoïsation, mais diffèrent sur ce que vous cherchez à mémoïser, sur les destinataires du cache, et sur les méthodes d’invalidation de ce cache.
useMemo
Vous devriez généralement utiliser useMemo
pour mettre en cache d’un rendu à l’autre un calcul coûteux dans un Composant Client. Ça pourrait par exemple mémoïser une transformation de données dans un composant.
'use client';
function WeatherReport({record}) {
const avgTemp = useMemo(() => calculateAvg(record)), record);
// ...
}
function App() {
const record = getRecord();
return (
<>
<WeatherReport record={record} />
<WeatherReport record={record} />
</>
);
}
Dans cet exemple, App
affiche deux WeatherReport
avec le même enregistrement. Même si les deux composants font le même travail, ils ne peuvent pas partager des traitements. Le cache de useMemo
est local à chaque composant.
En revanche, useMemo
s’assure bien que si App
refait un rendu et que l’objet record
n’a pas changé, chaque instance du composant évitera son calcul et utilisera plutôt sa valeur avgTemp
mémoïsée. useMemo
mettra le dernier calcul d’avgTemp
en cache sur base des dépendances qu’on lui fournit.
cache
Vous utiliserez cache
dans des Composants Serveur pour mémoïser du travail à partager entre plusieurs composants.
const cachedFetchReport = cache(fetchReport);
function WeatherReport({city}) {
const report = cachedFetchReport(city);
// ...
}
function App() {
const city = "Paris";
return (
<>
<WeatherReport city={city} />
<WeatherReport city={city} />
</>
);
}
En réécrivant l’exemple précédent pour utiliser cache
, cette fois la deuxième instance de WeatherReport
pourra s’éviter une duplication d’effort et lira depuis le même cache que le premier WeatherReport
. Une autre différence avec l’exemple précédent, c’est que cache
est également conseillée pour mémoïser des chargements de données, contrairement à useMemo
qui ne devrait être utilisée que pour des calculs.
Pour le moment, cache
ne devrait être utilisée que dans des Composants Serveur, et le cache sera invalidé à chaque requête serveur.
memo
Vous devriez utiliser memo
pour éviter qu’un composant ne recalcule son rendu alors que ses props n’ont pas changé.
'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} />
</>
);
}
Dans cet exemple, les deux composants MemoWeatherReport
appelleront calculateAvg
lors de leur premier rendu. Cependant, si App
refait son rendu, sans pour autant changer record
, aucune des props n’aura changé et MemoWeatherReport
ne refera pas son rendu.
Comparé à useMemo
, memo
mémoïse le rendu du composant sur base de ses props, au lieu de mémoïser des calculs spécifiques. Un peu comme avec useMemo
, le composant mémoïsé ne met en cache que le dernier rendu, avec les dernières valeurs de props. Dès que les props changent, le cache est invalidé et le composant refait son rendu.
Dépannage
Ma fonction mémoïsée est ré-exécutée alors que je l’ai appelée avec les mêmes arguments
Voyez déjà les pièges signalés plus haut :
- Appeler des fonctions mémoïsées distinctes lira des caches distincts
- Appeler une fonction mémoïsée hors d’un composant n’utilisera pas le cache
Si rien de tout ça ne s’applique, le problème peut être lié à la façon dont React vérifie l’existence de quelque chose dans le cache.
Si vos arguments ne sont pas des primitives (ce sont par exemple des objets, des fonctions, des tableaux), assurez-vous de toujours passer la même référence d’objet.
Lors d’un appel à une fonction mémoïsée, React utilisera les arguments passés pour déterminer si un résultat existe déjà dans le cache. React utilisera pour ce faire une comparaison superficielle des arguments.
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// 🚩 Erroné : les props sont un objet différent à chaque rendu.
const length = calculateNorm(props);
// ...
}
function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}
Dans le cas ci-dessus, les deux MapMarker
semblent faire exactement la même chose et appeler calculateNorm
avec les mêmes valeurs {x: 10, y: 10, z:10}
. Même si les objets contiennent des valeurs identiques, il ne s’agit pas d’une unique référence à un même objet, car chaque composant crée son propre objet props
.
React appellera Object.is
sur chaque argument pour vérifier l’existence dans le cache.
import {cache} from 'react';
const calculateNorm = cache((x, y, z) => {
// ...
});
function MapMarker(props) {
// ✅ Correct : passe des primitives à la fonction mémoïsée
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} />
</>
);
}
Une façon de remédier à ça consiste à passer les dimensions du vecteur à calculateNorm
. Ça fonctionne parce que chaque dimension passée est une valeur primitive.
Vous pourriez aussi passer l’objet vecteur lui-même comme prop au composant. Il vous faudrait toutefois passer le même objet en mémoire aux deux instances du composant.
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// ✅ Correct : passe le même objet `vector`
const length = calculateNorm(props.vector);
// ...
}
function App() {
const vector = [10, 10, 10];
return (
<>
<MapMarker vector={vector} />
<MapMarker vector={vector} />
</>
);
}