cache - Cette fonctionnalité est disponible dans le dernier Canary

Canary (fonctionnalité expérimentale)

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).

Remarque

L’optimisation qui consiste à mettre en cache les valeurs résultats sur base des arguments passés est généralement appelée mémoïsation. La fonction renvoyée par cache est dite « fonction mémoïsée ».

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’appeler cache 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. Si fn lève une exception pour certains arguments, ce sera mis en cache, et la même erreur sera levée lorsque cachedFn 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.

Piège

Appeler des fonctions mémoïsées distinctes lira des caches distincts

Pour partager un cache, des composants doivent appeler la même fonction mémoïsée.

// Temperature.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export function Temperature({cityData}) {
// 🚩 Erroné : Appeler `cache` au sein du composant
// crée une nouvelle `getWeekReport` à chaque rendu
const getWeekReport = cache(calculateWeekReport);
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 Erroné : `getWeekReport` n’est accessible que depuis
// le composant `Precipitation`.
const getWeekReport = cache(calculateWeekReport);

export function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

Dans l’exemple ci-dessus, Precipitation et Temperature appellent chacun cache pour créer une nouvelle fonction mémoïsée, qui dispose à chaque fois de son propre cache. Si les deux composants s’affichent avec les mêmes cityData, ils dupliqueront tout de même le travail en appelant à chaque fois calculateWeekReport.

Qui plus est, Temperature crée une nouvelle fonction mémoïsée à chaque rendu, ce qui ne permet aucun partage de cache.

Pour maximiser les correspondances trouvées et réduire la charge de calcul, les deux composants devraient s’assurer de partager la même fonction mémoïsée, pour pouvoir accéder au même cache. Définissez plutôt la fonction mémoïsée dans un module dédié qui peut faire l’objet d’un import dans les divers composants.

// getWeekReport.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export default cache(calculateWeekReport);
// Temperature.js
import getWeekReport from './getWeekReport';

export default function Temperature({cityData}) {
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import getWeekReport from './getWeekReport';

export default function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

Désormais les deux composants appellent la même fonction mémoïsée, exportée depuis ./getWeekReport.js, afin de lire et d’écrire dans le même 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.

Remarque

Le rendu asynchrone n’est possible que dans les Composants Serveur.

async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

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

Mettre en cache un traitement asynchrone

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.

Piège

Appeler une fonction mémoïsée hors d’un composant n’utilisera pas le cache

import {cache} from 'react';

const getUser = cache(async (userId) => {
return await db.user.query(userId);
});

// 🚩 Erroné : Appeler une fontion mémoïsée hors d’un composant
// n’exploitera pas le cache.
getUser('demo-id');

async function DemoProfile() {
// ✅ Correct : `getUser` exploitera le cache.
const user = await getUser('demo-id');
return <Profile user={user} />;
}

React ne fournit un accès au cache pour les fonctions mémoïsées qu’au sein d’un composant. Si vous appelez getUser hors d’un composant, il évaluera la fonction mais n’utilisera pas le cache (ni en lecture ni en écriture).

C’est parce que l’accès au cache est fourni via un contexte, et que les contextes ne sont accessibles que depuis les composants.

En détail

Comment choisir entre cache, memo et useMemo ?

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 :

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} />
</>
);
}