React.cache() : guide pratique pour Server Components
Publié le
Développeur React & Next.js freelance
Dans une application Next.js, plusieurs Server Components ont souvent besoin des mêmes données. L'utilisateur connecté, les permissions, une configuration. Sans mécanisme de déduplication, chaque composant déclenche sa propre requête. Résultat : des appels réseau dupliqués et un risque de données incohérentes entre composants.
React.cache() résout ces deux problèmes. Cette API, conçue pour les Server Components, mémoïse les appels de fonctions pour la durée d'un rendu serveur. Deux composants qui appellent la même fonction cachée avec les mêmes arguments reçoivent le même résultat, sans exécuter la fonction deux fois.
Dans cet article :
- Ce que fait
React.cache()et comment il fonctionne - Quand il est nécessaire et quand React gère déjà la déduplication
- Les deux cas d'usage : déduplication et consistance des données
- Les patterns concrets avec Next.js (preloading, routes dynamiques,
"use cache") - Les erreurs courantes et comment les éviter
Ce que fait React.cache()
Définition
React.cache() est une fonction du package react qui prend une fonction en paramètre et retourne sa version mémoïsée. Quand plusieurs composants appellent cette fonction mémoïsée avec les mêmes arguments pendant un même rendu serveur, la fonction originale n'est exécutée qu'une seule fois.
import { cache } from "react"
import { db } from "@/lib/db"
export const getUser = cache(async (userId: string) => {
return await db.user.findUnique({ where: { id: userId } })
})La documentation officielle React précise que cache compare les arguments avec Object.is. Pour les primitives (strings, numbers), la comparaison se fait par valeur : deux appels avec "abc" touchent le cache. Pour les objets, la comparaison se fait par référence : deux appels avec { id: "abc" } ne partagent pas le cache, car chaque objet littéral crée une nouvelle référence.
Signature
const cachedFn = cache(fn)| Paramètre | Type | Description |
|---|---|---|
fn | (...args: any[]) => any | La fonction à mémoïser. Peut être async. |
| Retour | Même type que fn | Version mémoïsée avec le même typage |
Cycle de vie du cache
Le cache créé par React.cache() a une durée de vie très courte :
- Création : au premier appel de la fonction mémoïsée pendant un rendu serveur
- Hit : tout appel ultérieur avec les mêmes arguments retourne le résultat en cache
- Invalidation : le cache est entièrement vidé à la fin de la requête HTTP
Ce comportement est fondamental. React.cache() n'est pas un cache persistant. Il ne survit pas entre deux requêtes. Chaque visiteur, chaque navigation, chaque rafraîchissement de page obtient des données fraîches.
Quand React.cache() est nécessaire (et quand il ne l'est pas)
Avant d'aller plus loin, un point essentiel : selon la façon dont une application Next.js accède aux données, React.cache() est soit indispensable, soit inutile.
Ce que React gère déjà
React déduplique automatiquement les appels fetch pendant un rendu serveur. Si generateMetadata et le composant de page appellent fetch("https://api.example.com/article/123") avec les mêmes options, une seule requête HTTP est envoyée.
// ✅ React déduplique automatiquement — pas besoin de React.cache()
export const generateMetadata = async ({ params }) => {
const { slug } = await params
const res = await fetch(`https://api.example.com/articles/${slug}`)
const article = await res.json()
return { title: article.title }
}
export default async function ArticlePage({ params }) {
const { slug } = await params
const res = await fetch(`https://api.example.com/articles/${slug}`)
const article = await res.json() // Même URL → dédupliqué par React
return <article>{article.title}</article>
}Ce qui nécessite React.cache()
Les accès directs à une base de données (ORM, SDK, requêtes SQL) ne passent pas par fetch. React ne les déduplique pas.
// ❌ Sans React.cache() : deux requêtes DB identiques
import { db } from "@/lib/db"
const getArticle = async (slug: string) => {
return await db.article.findUnique({ where: { slug } })
}
export const generateMetadata = async ({ params }) => {
const { slug } = await params
const article = await getArticle(slug) // 1ère requête DB
return { title: article.title }
}
export default async function ArticlePage({ params }) {
const { slug } = await params
const article = await getArticle(slug) // 2ème requête DB (dupliquée)
return <article>{article.title}</article>
}Arbre de décision
| Méthode d'accès aux données | Déduplication automatique | React.cache() nécessaire |
|---|---|---|
fetch (API REST, endpoints) | Oui (par React) | Non |
| ORM (Prisma, Drizzle, TypeORM) | Non | Oui |
| SDK tiers (Supabase client, Stripe) | Non | Oui |
| Accès DB direct (SQL, MongoDB driver) | Non | Oui |
| Calculs coûteux (transformations, agrégations) | Non | Oui, si appelés par plusieurs composants |
En résumé : si les données passent par fetch, la déduplication est déjà en place. Pour tout le reste, React.cache() est le mécanisme à utiliser.
Déduplication : la solution avec React.cache()
La section précédente montre le problème : sans React.cache(), chaque composant qui appelle la même fonction déclenche une requête séparée. Voici comment le résoudre, avec un layout et une page qui partagent les données utilisateur.
Module partagé + cache()
// ✅ Module partagé avec fonction cachée
import { cache } from "react"
import { db } from "@/lib/db"
export const getUser = cache(async (userId: string) => {
return await db.user.findUnique({ where: { id: userId } })
})import { getUser } from "@/data/get-user"
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const user = await getUser("user-123") // 1er appel → requête DB
return (
<div>
<nav>{user.name}</nav>
{children}
</div>
)
}import { getUser } from "@/data/get-user"
export default async function DashboardPage() {
const user = await getUser("user-123") // Cache hit → pas de requête
return <h1>Bienvenue, {user.name}</h1>
}La fonction getUser est définie une seule fois dans un module partagé. Chaque composant qui l'importe et l'appelle avec les mêmes arguments récupère le même résultat. Une seule requête pour tout le rendu.
Consistance : le bénéfice sous-estimé
La déduplication est le bénéfice visible. Mais le bénéfice le plus important de React.cache() est la consistance des données.
Le problème des Suspense boundaries
Avec les Server Components, React peut rendre différentes parties de l'arbre à des moments différents grâce aux Suspense boundaries. Deux composants qui fetchent les mêmes données indépendamment peuvent recevoir des résultats différents si les données changent entre les deux appels.
// ❌ Risque d'incohérence : les deux composants peuvent voir des données différentes
export default function DashboardPage() {
return (
<div>
<Suspense fallback={<Skeleton />}>
<SalesVolume /> {/* Rendu à t=0, lit 150 ventes */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<SalesRevenue /> {/* Rendu à t=200ms, lit 152 ventes */}
</Suspense>
</div>
)
}Si une vente est enregistrée entre le rendu de SalesVolume et celui de SalesRevenue, l'utilisateur voit un volume qui ne correspond pas au chiffre d'affaires. C'est du "UI tearing" (déchirement d'interface).
Avec React.cache(), les deux composants accèdent au même snapshot :
// ✅ Les deux composants voient les mêmes données
import { cache } from "react"
import { db } from "@/lib/db"
export const getSales = cache(async (period: string) => {
return await db.sale.findMany({
where: { period },
orderBy: { createdAt: "desc" },
})
})import { getSales } from "@/data/get-sales"
export const SalesVolume = async () => {
const sales = await getSales("2026-03")
return <p>{sales.length} ventes</p>
}import { getSales } from "@/data/get-sales"
export const SalesRevenue = async () => {
const sales = await getSales("2026-03")
const revenue = sales.reduce((sum, sale) => sum + sale.amount, 0)
return <p>{revenue.toFixed(2)} €</p>
}Le volume et le chiffre d'affaires sont toujours cohérents, même avec des Suspense boundaries séparées.
Cacher les fonctions impures
L'insight clé : React.cache() rend une fonction impure consistante pour la durée d'un rendu. Toute fonction qui accède à des données externes (base de données, API, new Date(), Math.random()) produit des résultats potentiellement différents à chaque appel. En la cachant, le résultat est figé pour le rendu en cours.
import { cache } from "react"
// ✅ Même timestamp pour tous les composants pendant un rendu
export const getCurrentTime = cache(() => new Date())Ce pattern est utile quand plusieurs composants doivent partager une référence temporelle commune (filtres par date, logs, timestamps d'affichage).
Patterns concrets avec Next.js
Pattern 1 : preloading
React.cache() permet de démarrer un fetch dans un composant parent, avant que le composant enfant qui en a besoin ne soit rendu. C'est le pattern de preloading, documenté dans la référence React.
import { cache } from "react"
import { db } from "@/lib/db"
export const getArticle = cache(async (slug: string) => {
return await db.article.findUnique({ where: { slug } })
})
export const preloadArticle = (slug: string) => {
void getArticle(slug)
}import { preloadArticle } from "@/data/get-article"
import { ArticleContent } from "@/components/article-content"
export default async function ArticlePage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
preloadArticle(slug) // Lance le fetch immédiatement
return <ArticleContent slug={slug} />
}import { getArticle } from "@/data/get-article"
export const ArticleContent = async ({ slug }: { slug: string }) => {
const article = await getArticle(slug) // Cache hit → résultat déjà disponible
return <article>{article.title}</article>
}Le composant parent lance le fetch avec preloadArticle (le void indique que la Promise n'est pas attendue ici). Quand ArticleContent appelle getArticle avec le même argument, le résultat est déjà disponible ou en cours de résolution. La requête n'est exécutée qu'une seule fois.
Pattern 2 : routes dynamiques avec cookies/headers
Quand une route Next.js utilise des Dynamic APIs (cookies(), headers(), searchParams), Next.js désactive le Full Route Cache : la page est rendue à chaque requête au lieu d'être servie depuis un cache statique. React.cache() n'est pas affecté (il opère pendant un rendu, pas entre les requêtes). Il devient même d'autant plus important : sans cache statique, la déduplication est le seul mécanisme qui évite les appels redondants.
import { cache } from "react"
import { cookies } from "next/headers"
export const getSession = cache(async () => {
const cookieStore = await cookies()
const token = cookieStore.get("session")?.value
if (!token) return null
return await verifyToken(token)
})import { getSession } from "@/data/get-session"
import { redirect } from "next/navigation"
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await getSession() // 1er appel → lit le cookie, vérifie le token
if (!session) redirect("/login")
return <div>{children}</div>
}import { getSession } from "@/data/get-session"
export default async function DashboardPage() {
const session = await getSession() // Cache hit → pas de re-vérification
return <h1>Bienvenue, {session!.user.name}</h1>
}La vérification du token ne s'exécute qu'une fois, même si getSession est appelé dans le layout, la page et les composants enfants.
Pattern 3 : combiner React.cache() et "use cache"
React.cache() et "use cache" (Next.js 16) résolvent des problèmes différents et se combinent naturellement.
import { cache } from "react"
// "use cache" : le résultat persiste entre les requêtes
const fetchProducts = async (categoryId: string) => {
"use cache"
return await db.product.findMany({ where: { categoryId } })
}
// React.cache() : déduplique pendant un rendu
export const getProducts = cache(fetchProducts)| Couche | Rôle | Durée de vie |
|---|---|---|
React.cache() | Déduplication + consistance | Un rendu serveur |
"use cache" | Persistance entre requêtes | Configurable (revalidation) |
React.cache() garantit que pendant un rendu, tous les composants voient les mêmes produits. "use cache" évite de re-fetcher la liste à chaque requête HTTP.
React.cache() vs useMemo vs memo
Ces trois API ont des rôles distincts. Les confondre mène à des usages incorrects.
| Critère | React.cache() | useMemo | memo |
|---|---|---|---|
| Environnement | Server Components | Client Components | Client Components |
| Portée | Partagé entre composants | Local au composant | Par composant |
| Durée de vie | Une requête HTTP | Entre les renders du composant | Entre les renders si props identiques |
| Cas d'usage | Déduplication data fetching | Calculs coûteux côté client | Éviter les re-renders |
Depuis React 19, le React Compiler gère automatiquement la mémoïsation côté client. useMemo et memo sont rarement nécessaires dans les nouveaux projets. React.cache() reste indispensable côté serveur car le Compiler n'optimise pas les appels réseau.
Les erreurs à éviter
Déclarer cache() dans un composant
L'erreur la plus fréquente. Si cache() est appelé dans le corps d'un composant, une nouvelle fonction mémoïsée est créée à chaque render. Aucune déduplication.
// ❌ Nouvelle fonction cachée à chaque render → pas de déduplication
export const Dashboard = async ({ userId }: { userId: string }) => {
const getUser = cache(async (id: string) => {
return await db.user.findUnique({ where: { id } })
})
const user = await getUser(userId)
return <h1>{user.name}</h1>
}La solution : déclarer la fonction cachée au niveau du module, comme dans l'exemple src/data/get-user.ts de la section précédente. Tous les composants qui importent la même fonction partagent le même cache.
Passer des objets comme arguments
React.cache() utilise Object.is pour comparer les arguments. Deux objets avec le même contenu mais des références différentes sont considérés comme différents.
// ❌ Objet inline → nouvelle référence à chaque appel → jamais de cache hit
const products = await getProducts({ status: "active", limit: 10 })// ✅ Primitives → même valeur = cache hit
const products = await getProducts("active", 10)Si un objet est nécessaire, passer la même référence depuis un composant parent ou sérialiser les arguments en chaîne de caractères.
Confondre React.cache() avec un cache persistant
React.cache() ne persiste rien entre les requêtes. Si la même page est visitée deux fois, la fonction s'exécute deux fois.
// ❌ Attente incorrecte : "ça va cacher entre les visites"
export const getConfig = cache(async () => {
return await fetch("https://api.example.com/config").then((r) => r.json())
})Pour persister entre les requêtes, utiliser "use cache" (Next.js 16) ou un cache externe (Redis, CDN).
Appeler une fonction cachée en dehors d'un composant
Le cache de React.cache() n'est accessible que pendant un rendu React. Appeler la fonction dans un middleware, une Route Handler ou un script ne bénéficie d'aucune déduplication.
// ❌ Pas de contexte de rendu React → le cache ne fonctionne pas
import { getSession } from "@/data/get-session"
export async function middleware(request: NextRequest) {
const session = await getSession() // Exécution normale, sans cache
// ...
}La fonction s'exécute normalement, mais chaque appel déclenche une nouvelle exécution. Pour un middleware ou une Route Handler, implémenter un mécanisme de cache séparé si la déduplication est nécessaire.
FAQ
Quelle différence entre React.cache() et la déduplication automatique de fetch ?
React déduplique automatiquement les appels fetch ayant la même URL et les mêmes options pendant un rendu serveur. React.cache() fait la même chose pour n'importe quelle fonction (requêtes ORM, calculs, appels SDK). Si toutes les données passent par fetch, la déduplication est déjà en place. Pour Prisma, Drizzle, ou tout accès direct à une base de données, React.cache() est nécessaire.
React.cache() fonctionne-t-il côté client ?
Non. React.cache() est conçu exclusivement pour les Server Components. Côté client, useMemo mémoïse un calcul au sein d'un composant, et memo évite les re-renders. Depuis React 19, le React Compiler automatise ces optimisations dans la plupart des cas.
Faut-il utiliser React.cache() sur toutes les fonctions de data fetching ?
Non, uniquement sur celles appelées par plusieurs composants pendant un même rendu. Une fonction appelée une seule fois (dans une page sans layout qui la partage) n'a pas besoin de cache(). L'ajouter systématiquement ne cause pas de problème de performance, mais ajoute une indirection inutile.
React.cache() gère-t-il les erreurs ?
Oui, les erreurs sont aussi cachées. Si la fonction lance une erreur, cette même erreur est relancée pour tous les composants qui appellent la fonction avec les mêmes arguments. Ce comportement est cohérent avec le principe de consistance : tous les composants reçoivent le même résultat, succès ou échec.
Comment invalider le cache de React.cache() ?
Le cache s'invalide automatiquement à la fin de chaque requête HTTP. Il n'existe pas de mécanisme d'invalidation manuelle, car le cache ne dure que le temps d'un rendu serveur. Pour invalider un cache persistant ("use cache"), utiliser revalidateTag() ou revalidatePath() depuis Next.js.
Conclusion
Points clés
React.cache()déduplique les appels de fonctions pendant un rendu serveur, pas entre les requêtes- Le bénéfice principal est la consistance : tous les composants voient le même snapshot de données
- Déclarer les fonctions cachées dans des modules partagés, jamais dans un composant
- Passer des primitives comme arguments (les objets créent des références différentes)
- Combiner avec
"use cache"pour couvrir les deux besoins : déduplication intra-requête et persistance inter-requêtes
Checklist
- Les fonctions de data fetching partagées entre composants utilisent
React.cache() - Les fonctions cachées sont déclarées dans des modules séparés (
src/data/) - Les arguments sont des primitives (strings, numbers) pour garantir les cache hits
React.cache()n'est pas confondu avec un cache persistant- Les routes dynamiques (
cookies(),headers()) utilisentReact.cache()pour dédupliquer

À propos de l'auteur
Je suis Dimitri Dumont, développeur freelance spécialisé React & Next.js depuis plus de 7 ans. J'ai accompagné 22 startups et réalisé 43 missions avec une note de 5/5. Je partage ici les pratiques que j'utilise en mission. En savoir plus →
Une question sur cet article ?
Cet article vous a été utile ?
Je peux vous accompagner sur votre projet React & Next.js.
Discutons de votre projet →