Aller au contenu principal

React.cache() : guide pratique pour Server Components

Publié le

Dimitri Dumont
Dimitri Dumont

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.

src/data/get-user.ts
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ètreTypeDescription
fn(...args: any[]) => anyLa fonction à mémoïser. Peut être async.
RetourMême type que fnVersion 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 :

  1. Création : au premier appel de la fonction mémoïsée pendant un rendu serveur
  2. Hit : tout appel ultérieur avec les mêmes arguments retourne le résultat en cache
  3. 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.

src/app/blog/[slug]/page.tsx
// ✅ 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.

src/app/blog/[slug]/page.tsx
// ❌ 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éesDéduplication automatiqueReact.cache() nécessaire
fetch (API REST, endpoints)Oui (par React)Non
ORM (Prisma, Drizzle, TypeORM)NonOui
SDK tiers (Supabase client, Stripe)NonOui
Accès DB direct (SQL, MongoDB driver)NonOui
Calculs coûteux (transformations, agrégations)NonOui, 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()

src/data/get-user.ts
// ✅ 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 } })
})
src/app/dashboard/layout.tsx
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>
  )
}
src/app/dashboard/page.tsx
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.

src/app/dashboard/page.tsx
// ❌ 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 :

src/data/get-sales.ts
// ✅ 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" },
  })
})
src/components/sales-volume.tsx
import { getSales } from "@/data/get-sales"
 
export const SalesVolume = async () => {
  const sales = await getSales("2026-03")
  return <p>{sales.length} ventes</p>
}
src/components/sales-revenue.tsx
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.

src/data/get-current-time.ts
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.

src/data/get-article.ts
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)
}
src/app/blog/[slug]/page.tsx
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} />
}
src/components/article-content.tsx
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.

src/data/get-session.ts
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)
})
src/app/dashboard/layout.tsx
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>
}
src/app/dashboard/page.tsx
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.

src/data/get-products.ts
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)
CoucheRôleDurée de vie
React.cache()Déduplication + consistanceUn rendu serveur
"use cache"Persistance entre requêtesConfigurable (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èreReact.cache()useMemomemo
EnvironnementServer ComponentsClient ComponentsClient Components
PortéePartagé entre composantsLocal au composantPar composant
Durée de vieUne requête HTTPEntre les renders du composantEntre les renders si props identiques
Cas d'usageDéduplication data fetchingCalculs 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.

src/components/dashboard.tsx
// ❌ 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.

src/middleware.ts
// ❌ 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

  1. React.cache() déduplique les appels de fonctions pendant un rendu serveur, pas entre les requêtes
  2. Le bénéfice principal est la consistance : tous les composants voient le même snapshot de données
  3. Déclarer les fonctions cachées dans des modules partagés, jamais dans un composant
  4. Passer des primitives comme arguments (les objets créent des références différentes)
  5. 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()) utilisent React.cache() pour dédupliquer
Dimitri Dumont

À 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 →

Articles similaires