Maxpaths
Fondamentaux·Section 1/10

Pourquoi vos optimisations ralentissent votre app ?

React fournit trois outils de memoisation pour optimiser les performances de vos applications : React.memo, useMemo et useCallback. Chacun intervient a un niveau different du cycle de rendu. Comprendre quand et pourquoi les utiliser est essentiel pour ecrire du code React performant sans tomber dans le piege de la sur-optimisation.

Le trio de la memoisation

React propose trois mecanismes complementaires. Chacun resout un probleme specifique dans le pipeline de rendu.

React.memo

Higher-Order Component qui empeche le re-render d'un composant si ses props n'ont pas change. Agit au niveau du composant entier.

useMemo

Hook qui met en cache le resultat d'un calcul entre les renders. Agit au niveau d'une valeur calculee.

useCallback

Hook qui met en cache une definition de fonction entre les renders. Agit au niveau d'une reference de fonction.

Pourquoi la memoisation existe-t-elle ?

Par defaut, quand un composant parent se re-rend, tous ses enfants se re-rendent egalement, meme si leurs props n'ont pas change. Dans la plupart des cas, React est suffisamment rapide pour que cela ne pose aucun probleme. Mais lorsqu'un composant effectue un calcul couteux, rend une longue liste, ou passe des callbacks a des enfants complexes, ces re-renders inutiles peuvent degrader l'experience utilisateur.

dashboard.tsxtsx
1// Probleme : ce composant se re-rend a chaque changement du parent
2// meme si "user" n'a pas change
3
4interface UserCardProps {
5 user: { name: string; email: string };
6}
7
8function UserCard({ user }: UserCardProps) {
9 // Ce console.log s'affiche a CHAQUE render du parent
10 console.log('UserCard render:', user.name);
11
12 // Imaginons un calcul couteux ici...
13 const formattedDate = new Intl.DateTimeFormat('fr-FR', {
14 dateStyle: 'full',
15 timeStyle: 'long',
16 }).format(new Date());
17
18 return (
19 <div className="p-4 border rounded-lg">
20 <h3>{user.name}</h3>
21 <p>{user.email}</p>
22 <span>{formattedDate}</span>
23 </div>
24 );
25}
26
27// Le parent : chaque clic sur le bouton re-rend UserCard
28function Dashboard() {
29 const [count, setCount] = useState(0);
30 const user = { name: 'Alice Dupont', email: 'alice@example.com' };
31
32 return (
33 <div>
34 {/* Ce bouton modifie count, pas user */}
35 <button onClick={() => setCount(c => c + 1)}>
36 Compteur : {count}
37 </button>
38
39 {/* Pourtant UserCard se re-rend a chaque clic */}
40 <UserCard user={user} />
41 </div>
42 );
43}

Ce que vous apprendrez dans ce guide

Un parcours complet pour maitriser la memoisation React, de la theorie a la pratique en production.

  • 01Comprendre le mecanisme de re-render et identifier quand il devient problematique
  • 02Maitriser React.memo et la comparaison superficielle des props
  • 03Utiliser useMemo pour les calculs couteux et la stabilisation de references
  • 04Appliquer useCallback pour stabiliser les fonctions passees en props
  • 05Combiner les trois outils dans un scenario complet de production
  • 06Eviter les erreurs courantes et savoir quand ne pas memoiser
  • 07Decouvrir le React Compiler et l'avenir de la memoisation automatique
Fondamentaux·Section 2/10

Qu'est-ce qui déclenche vraiment un re-render ?

Avant de memoiser quoi que ce soit, il faut comprendre comment React decide de re-rendre un composant. Le mecanisme est simple : quand un etat change, React re-execute le composant et tous ses descendants. Mais re-render ne signifie pas forcement re-paint du DOM reel. Cette distinction est fondamentale.

Le cycle de rendu React

Quand un composant appelle setState, React declenche une cascade : le composant se re-execute, produit un nouveau Virtual DOM, React compare l'ancien et le nouveau (reconciliation), puis applique uniquement les differences au DOM reel. Les enfants suivent le meme processus, meme si leurs props sont identiques.

cascade-demo.tsxtsx
1// Demonstration : la cascade de re-renders
2
3function App() {
4 const [count, setCount] = useState(0);
5 console.log('App render'); // S'affiche a chaque clic
6
7 return (
8 <div>
9 <button onClick={() => setCount(c => c + 1)}>
10 Incrementer ({count})
11 </button>
12 <Parent />
13 </div>
14 );
15}
16
17function Parent() {
18 console.log(' Parent render'); // S'affiche aussi !
19 return (
20 <div>
21 <Child />
22 <Sibling />
23 </div>
24 );
25}
26
27function Child() {
28 console.log(' Child render'); // S'affiche aussi !
29 return <p>Je suis Child</p>;
30}
31
32function Sibling() {
33 console.log(' Sibling render'); // S'affiche aussi !
34 return <p>Je suis Sibling</p>;
35}
36
37// Console apres un clic sur "Incrementer" :
38// App render
39// Parent render
40// Child render
41// Sibling render
42//
43// Tous les composants se re-executent, meme si
44// aucun d'entre eux ne recoit "count" en prop.

Re-render et re-paint : deux choses differentes

Un re-render React (execution de la fonction composant) ne signifie pas que le navigateur va redessiner les pixels. React compare le Virtual DOM ancien et nouveau, et ne touche au DOM reel que si quelque chose a change.

Re-render (Virtual DOM)

  • La fonction composant est re-executee
  • Un nouveau Virtual DOM est produit
  • React compare ancien et nouveau
  • Cout : execution JavaScript en memoire

Re-paint (DOM reel)

  • Le navigateur redessine des pixels
  • Ne se produit que si le DOM est modifie
  • React applique le diff minimal
  • Cout : layout, paint, composite du navigateur
store-page.tsxtsx
1// Exemple concret : composant "lourd" avec un calcul couteux
2
3interface ProductListProps {
4 products: Product[];
5 category: string;
6}
7
8function ProductList({ products, category }: ProductListProps) {
9 console.log('ProductList render - debut du calcul...');
10
11 // Simulation d'un traitement couteux : filtrage + tri + transformation
12 // Sur 10 000 produits, ce calcul prend ~50ms
13 const filteredProducts = products
14 .filter(p => p.category === category)
15 .sort((a, b) => b.rating - a.rating)
16 .map(p => ({
17 ...p,
18 displayPrice: new Intl.NumberFormat('fr-FR', {
19 style: 'currency',
20 currency: 'EUR',
21 }).format(p.price),
22 slug: p.name.toLowerCase().replace(/\s+/g, '-'),
23 }));
24
25 console.log(`ProductList render - ${filteredProducts.length} produits traites`);
26
27 return (
28 <ul>
29 {filteredProducts.map(product => (
30 <li key={product.id}>
31 {product.name} - {product.displayPrice}
32 </li>
33 ))}
34 </ul>
35 );
36}
37
38// Parent : le compteur de notifications n'a RIEN a voir avec les produits
39function StorePage() {
40 const [notifications, setNotifications] = useState(0);
41 const [products] = useState<Product[]>(generateProducts(10000));
42
43 return (
44 <div>
45 <header>
46 <button onClick={() => setNotifications(n => n + 1)}>
47 Notifications ({notifications})
48 </button>
49 </header>
50
51 {/* Ce composant refait le calcul couteux a chaque clic
52 sur le bouton notifications, pour rien. */}
53 <ProductList products={products} category="electronique" />
54 </div>
55 );
56}

Re-render n'est pas toujours un probleme

React est extremement rapide. La reconciliation du Virtual DOM est optimisee pour traiter des milliers de composants en quelques millisecondes. Ne memoiser que lorsque vous avez mesure un probleme reel.

  • --Un composant simple (texte, image, layout) se re-rend en moins de 0,1 ms
  • --React ne met a jour le DOM reel que si le Virtual DOM a effectivement change
  • --La memoisation a un cout : comparaison des deps, memoire supplementaire, complexite du code
  • --Regle d'or : mesurer d'abord avec le Profiler, optimiser ensuite
profiling-guide.tsxtsx
1// Identifier les re-renders couteux avec React DevTools Profiler
2//
3// Etape 1 : Ouvrir React DevTools > onglet "Profiler"
4// Etape 2 : Cliquer sur "Record" (bouton rond)
5// Etape 3 : Effectuer l'interaction a analyser (clic, saisie, etc.)
6// Etape 4 : Cliquer sur "Stop"
7// Etape 5 : Analyser le flamegraph
8
9// Ce que vous voyez dans le flamegraph :
10//
11// [App] 0.2ms
12// [StorePage] 0.3ms
13// [Header] 0.1ms
14// [ProductList] 47.3ms <-- PROBLEME ICI
15// [ProductCard] x 842
16//
17// La barre "ProductList" est jaune/rouge = composant lent
18// Les composants gris n'ont pas ete re-rendus
19//
20// Criteres pour decider d'optimiser :
21// - Composant > 16ms (1 frame a 60fps) = a optimiser
22// - Composant > 5ms avec interactions frequentes = a surveiller
23// - Composant < 1ms = ne pas toucher
24
25// Alternative programmatique : React.Profiler
26import { Profiler } from 'react';
27
28function onRenderCallback(
29 id: string,
30 phase: 'mount' | 'update',
31 actualDuration: number, // temps de render en ms
32 baseDuration: number, // temps sans memoisation
33 startTime: number,
34 commitTime: number,
35) {
36 if (actualDuration > 16) {
37 console.warn(
38 `[Perf] ${id} a pris ${actualDuration.toFixed(1)}ms (${phase})`
39 );
40 }
41}
42
43// Utilisation :
44<Profiler id="ProductList" onRender={onRenderCallback}>
45 <ProductList products={products} category="electronique" />
46</Profiler>

Quand les re-renders deviennent un vrai probleme

Les re-renders deviennent problematiques dans trois scenarios precis : lorsqu'un composant effectue un calcul couteux a chaque render (filtrage, tri, formatage de grandes listes), lorsqu'un composant rend un grand nombre d'enfants (liste de centaines d'elements), ou lorsque les re-renders se produisent a haute frequence (frappe clavier, mouvement de souris, scroll). Dans le reste des cas, faites confiance a React : il est deja optimise pour gerer les re-renders courants.

Modes de Rendu·Section 3/10

Comment éviter les re-renders inutiles de composants ?

React.memo est un Higher-Order Component (HOC) qui enveloppe un composant et empeche son re-render si ses props n'ont pas change. C'est l'outil le plus visible du trio de memoisation : il agit au niveau du composant entier et decide si l'execution de la fonction composant peut etre sautee.

user-card.tsxtsx
1// Syntaxe de base : envelopper un composant avec React.memo
2
3import { memo } from 'react';
4
5interface UserCardProps {
6 name: string;
7 email: string;
8 role: string;
9}
10
11// Sans memo : se re-rend a CHAQUE render du parent
12function UserCard({ name, email, role }: UserCardProps) {
13 console.log('UserCard render:', name);
14 return (
15 <div className="p-4 border rounded-lg">
16 <h3 className="font-bold">{name}</h3>
17 <p className="text-gray-600">{email}</p>
18 <span className="text-sm bg-blue-100 px-2 py-1 rounded">{role}</span>
19 </div>
20 );
21}
22
23// Avec memo : ne se re-rend QUE si name, email ou role changent
24const MemoizedUserCard = memo(function UserCard({
25 name,
26 email,
27 role,
28}: UserCardProps) {
29 console.log('MemoizedUserCard render:', name);
30 return (
31 <div className="p-4 border rounded-lg">
32 <h3 className="font-bold">{name}</h3>
33 <p className="text-gray-600">{email}</p>
34 <span className="text-sm bg-blue-100 px-2 py-1 rounded">{role}</span>
35 </div>
36 );
37});
38
39// Export direct (pattern le plus courant)
40export default memo(UserCard);

Comparaison superficielle (shallow comparison)

Par defaut, React.memo effectue une comparaison superficielle des props : il verifie l'egalite par reference (===) pour chaque prop. Pour les types primitifs (string, number, boolean), cela fonctionne parfaitement. Pour les objets et fonctions, c'est la que les pieges commencent.

stable-primitives.tsxtsx
1// CAS 1 : Props primitives - React.memo FONCTIONNE parfaitement
2
3import { memo, useState } from 'react';
4
5const ExpensiveChart = memo(function ExpensiveChart({
6 title,
7 value,
8 showLegend,
9}: {
10 title: string; // Primitive : comparaison par valeur
11 value: number; // Primitive : comparaison par valeur
12 showLegend: boolean; // Primitive : comparaison par valeur
13}) {
14 console.log('ExpensiveChart render');
15 // Simulation d'un rendu couteux (graphique SVG complexe)
16 const paths = Array.from({ length: 1000 }, (_, i) => (
17 <path key={i} d={`M${i} ${Math.sin(i) * value}`} />
18 ));
19
20 return (
21 <div>
22 <h3>{title}</h3>
23 <svg>{paths}</svg>
24 {showLegend && <Legend />}
25 </div>
26 );
27});
28
29function Dashboard() {
30 const [notifications, setNotifications] = useState(0);
31
32 return (
33 <div>
34 <button onClick={() => setNotifications(n => n + 1)}>
35 Notifications ({notifications})
36 </button>
37
38 {/* "Revenue mensuelle", 42000 et true ne changent pas
39 entre les renders -> ExpensiveChart ne se re-rend PAS */}
40 <ExpensiveChart
41 title="Revenue mensuelle" // "Revenue mensuelle" === "Revenue mensuelle" -> true
42 value={42000} // 42000 === 42000 -> true
43 showLegend={true} // true === true -> true
44 />
45 {/* Resultat : le clic sur Notifications ne declenche PAS
46 le re-render de ExpensiveChart */}
47 </div>
48 );
49}
inline-objects-problem.tsxtsx
1// CAS 2 : Objets et fonctions inline - React.memo est CONTOURNE
2
3import { memo, useState } from 'react';
4
5const UserProfile = memo(function UserProfile({
6 user,
7 onEdit,
8}: {
9 user: { name: string; age: number };
10 onEdit: () => void;
11}) {
12 console.log('UserProfile render'); // S'affiche a CHAQUE render du parent !
13 return (
14 <div>
15 <h3>{user.name}, {user.age} ans</h3>
16 <button onClick={onEdit}>Modifier</button>
17 </div>
18 );
19});
20
21function App() {
22 const [count, setCount] = useState(0);
23
24 return (
25 <div>
26 <button onClick={() => setCount(c => c + 1)}>
27 Compteur : {count}
28 </button>
29
30 <UserProfile
31 // PROBLEME 1 : objet literal = nouvelle reference a chaque render
32 // { name: 'Alice', age: 30 } !== { name: 'Alice', age: 30 }
33 // car ce sont deux objets differents en memoire
34 user={{ name: 'Alice', age: 30 }}
35
36 // PROBLEME 2 : fonction inline = nouvelle reference a chaque render
37 // () => {} !== () => {}
38 // car ce sont deux fonctions differentes en memoire
39 onEdit={() => console.log('edit')}
40 />
41 {/* memo compare les props par reference (===)
42 Objet precedent !== Nouvel objet -> re-render
43 Fonction precedente !== Nouvelle fonction -> re-render
44 Resultat : memo est completement inutile ici */}
45 </div>
46 );
47}

Comparateur personnalise (areEqual)

Pour les cas ou la comparaison superficielle ne suffit pas, React.memo accepte un second argument : une fonction de comparaison personnalisee. Cette fonction recoit les anciennes et nouvelles props et retourne true si le composant ne doit PAS se re-rendre.

data-grid.tsxtsx
1// Comparateur personnalise : controle fin sur la comparaison
2
3import { memo } from 'react';
4
5interface DataGridProps {
6 data: Array<{ id: string; value: number; label: string }>;
7 columns: string[];
8 onRowClick: (id: string) => void;
9 lastUpdated: Date;
10}
11
12// areEqual retourne true = PAS de re-render
13// areEqual retourne false = re-render
14function areEqual(prevProps: DataGridProps, nextProps: DataGridProps): boolean {
15 // Comparer la longueur et les IDs des donnees (pas la reference)
16 if (prevProps.data.length !== nextProps.data.length) return false;
17
18 // Verifier si les donnees ont reellement change (deep comparison ciblee)
19 const dataChanged = prevProps.data.some((item, index) => {
20 const next = nextProps.data[index];
21 return item.id !== next.id || item.value !== next.value;
22 });
23 if (dataChanged) return false;
24
25 // Les colonnes changent rarement, comparer par longueur + contenu
26 if (prevProps.columns.length !== nextProps.columns.length) return false;
27 if (prevProps.columns.some((col, i) => col !== nextProps.columns[i])) return false;
28
29 // Ignorer onRowClick (sera stabilise par useCallback dans le parent)
30 // Ignorer lastUpdated si la difference est < 1 seconde
31 const timeDiff = Math.abs(
32 nextProps.lastUpdated.getTime() - prevProps.lastUpdated.getTime()
33 );
34 if (timeDiff > 1000) return false;
35
36 return true; // Props considerees identiques, pas de re-render
37}
38
39const DataGrid = memo(function DataGrid({
40 data,
41 columns,
42 onRowClick,
43 lastUpdated,
44}: DataGridProps) {
45 console.log('DataGrid render -', data.length, 'lignes');
46
47 return (
48 <table>
49 <thead>
50 <tr>
51 {columns.map(col => <th key={col}>{col}</th>)}
52 </tr>
53 </thead>
54 <tbody>
55 {data.map(row => (
56 <tr key={row.id} onClick={() => onRowClick(row.id)}>
57 <td>{row.label}</td>
58 <td>{row.value}</td>
59 </tr>
60 ))}
61 </tbody>
62 </table>
63 );
64}, areEqual); // <-- second argument : la fonction de comparaison
65
66export default DataGrid;

React.memo est un HOC, pas un hook

Contrairement a useMemo et useCallback, React.memo n'est pas un hook. C'est un Higher-Order Component : une fonction qui prend un composant et retourne un nouveau composant. Cette distinction est importante.

HOC (React.memo)

  • Enveloppe un composant entier
  • Decide si le render est necessaire
  • S'applique a l'exterieur du composant
  • Fonctionne avec les composants classes et fonctions
  • Compare les props entre deux renders

Hooks (useMemo, useCallback)

  • Agissent a l'interieur d'un composant
  • Mettent en cache des valeurs ou fonctions
  • S'utilisent dans le corps de la fonction composant
  • Fonctionnent uniquement avec les composants fonctions
  • Comparent un tableau de dependances
Modes de Rendu·Section 4/10

Quand mémoiser un calcul coûteux ?

useMemo est un hook qui met en cache le resultat d'un calcul entre les renders. Il re-execute le calcul uniquement lorsque ses dependances changent. Ses deux cas d'usage principaux : eviter des calculs couteux et stabiliser des references d'objets pour que React.memo fonctionne correctement.

product-catalog.tsxtsx
1// Cas d'usage 1 : memoiser un calcul couteux
2
3import { useMemo, useState } from 'react';
4
5interface Product {
6 id: string;
7 name: string;
8 price: number;
9 category: string;
10 rating: number;
11 inStock: boolean;
12}
13
14function ProductCatalog({ products }: { products: Product[] }) {
15 const [search, setSearch] = useState('');
16 const [sortBy, setSortBy] = useState<'price' | 'rating'>('price');
17 const [showInStockOnly, setShowInStockOnly] = useState(false);
18
19 // Sans useMemo : ce calcul s'execute a CHAQUE render
20 // Avec 10 000 produits, c'est ~50ms a chaque frappe clavier
21 const filteredAndSorted = useMemo(() => {
22 console.log('Recalcul de la liste filtree...');
23
24 // Etape 1 : Filtrer par recherche
25 let result = products.filter(p =>
26 p.name.toLowerCase().includes(search.toLowerCase())
27 );
28
29 // Etape 2 : Filtrer par stock
30 if (showInStockOnly) {
31 result = result.filter(p => p.inStock);
32 }
33
34 // Etape 3 : Trier
35 result.sort((a, b) => {
36 if (sortBy === 'price') return a.price - b.price;
37 return b.rating - a.rating;
38 });
39
40 // Etape 4 : Formater les prix (couteux avec Intl)
41 return result.map(p => ({
42 ...p,
43 displayPrice: new Intl.NumberFormat('fr-FR', {
44 style: 'currency',
45 currency: 'EUR',
46 }).format(p.price),
47 }));
48 }, [products, search, sortBy, showInStockOnly]);
49 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 // Le calcul ne se relance QUE si l'une de ces 4 valeurs change.
51 // Si un autre state du composant change (ex: un modal ouvert),
52 // useMemo retourne le resultat en cache.
53
54 return (
55 <div>
56 <input
57 value={search}
58 onChange={e => setSearch(e.target.value)}
59 placeholder="Rechercher..."
60 />
61 <select
62 value={sortBy}
63 onChange={e => setSortBy(e.target.value as 'price' | 'rating')}
64 >
65 <option value="price">Prix</option>
66 <option value="rating">Note</option>
67 </select>
68 <label>
69 <input
70 type="checkbox"
71 checked={showInStockOnly}
72 onChange={e => setShowInStockOnly(e.target.checked)}
73 />
74 En stock uniquement
75 </label>
76
77 <p>{filteredAndSorted.length} resultats</p>
78 <ul>
79 {filteredAndSorted.map(p => (
80 <li key={p.id}>{p.name} - {p.displayPrice}</li>
81 ))}
82 </ul>
83 </div>
84 );
85}

Le piege des references : pourquoi useMemo est indispensable pour React.memo

En JavaScript, deux objets avec le meme contenu ne sont pas egaux par reference : { color: 'blue' } !== { color: 'blue' }. A chaque render, un objet literal cree une nouvelle reference. Si cet objet est passe en prop a un composant enveloppe par React.memo, le memo est contourne car la reference a change. useMemo resout ce probleme.

reference-trap.tsxtsx
1// Le piege de la reference : sans useMemo, React.memo est inutile
2
3import { memo, useMemo, useState } from 'react';
4
5// Composant enfant memoize
6const ChartConfig = memo(function ChartConfig({
7 config,
8}: {
9 config: { theme: string; animate: boolean; gridLines: number };
10}) {
11 console.log('ChartConfig render');
12 return (
13 <div>
14 Theme: {config.theme}, Grille: {config.gridLines} lignes
15 </div>
16 );
17});
18
19// PROBLEME : sans useMemo
20function DashboardBroken() {
21 const [refreshCount, setRefreshCount] = useState(0);
22
23 // Cet objet est recree a CHAQUE render = nouvelle reference
24 // { theme: 'dark', ... } !== { theme: 'dark', ... } par reference
25 const config = { theme: 'dark', animate: true, gridLines: 5 };
26
27 return (
28 <div>
29 <button onClick={() => setRefreshCount(r => r + 1)}>
30 Rafraichir ({refreshCount})
31 </button>
32 {/* memo est contourne : config est une nouvelle reference */}
33 <ChartConfig config={config} />
34 </div>
35 );
36}
37
38// SOLUTION : avec useMemo
39function DashboardFixed() {
40 const [refreshCount, setRefreshCount] = useState(0);
41
42 // useMemo retourne la MEME reference tant que les deps ne changent pas
43 const config = useMemo(
44 () => ({ theme: 'dark', animate: true, gridLines: 5 }),
45 [] // Pas de dependances = reference stable pour toute la vie du composant
46 );
47
48 return (
49 <div>
50 <button onClick={() => setRefreshCount(r => r + 1)}>
51 Rafraichir ({refreshCount})
52 </button>
53 {/* memo fonctionne : config est la meme reference */}
54 <ChartConfig config={config} />
55 </div>
56 );
57}
employee-directory.tsxtsx
1// Exemple complet : liste de 10 000 elements avec filtre memoize
2
3import { memo, useMemo, useState } from 'react';
4
5interface Employee {
6 id: string;
7 name: string;
8 department: string;
9 salary: number;
10 hireDate: string;
11}
12
13// Composant ligne memoize (ne re-rend que si l'employe change)
14const EmployeeRow = memo(function EmployeeRow({
15 employee,
16 isSelected,
17 onSelect,
18}: {
19 employee: Employee;
20 isSelected: boolean;
21 onSelect: (id: string) => void;
22}) {
23 console.log('EmployeeRow render:', employee.name);
24 return (
25 <tr
26 className={isSelected ? 'bg-blue-50' : ''}
27 onClick={() => onSelect(employee.id)}
28 >
29 <td>{employee.name}</td>
30 <td>{employee.department}</td>
31 <td>
32 {new Intl.NumberFormat('fr-FR', {
33 style: 'currency',
34 currency: 'EUR',
35 }).format(employee.salary)}
36 </td>
37 <td>{new Date(employee.hireDate).toLocaleDateString('fr-FR')}</td>
38 </tr>
39 );
40});
41
42function EmployeeDirectory({ employees }: { employees: Employee[] }) {
43 const [filter, setFilter] = useState('');
44 const [selectedId, setSelectedId] = useState<string | null>(null);
45 const [sortField, setSortField] = useState<'name' | 'salary'>('name');
46
47 // useMemo : le filtre + tri ne se recalcule que lorsque
48 // employees, filter ou sortField changent.
49 // Cliquer sur une ligne (selectedId change) ne relance PAS le calcul.
50 const visibleEmployees = useMemo(() => {
51 console.log('Recalcul de visibleEmployees...');
52
53 const filtered = employees.filter(emp =>
54 emp.name.toLowerCase().includes(filter.toLowerCase()) ||
55 emp.department.toLowerCase().includes(filter.toLowerCase())
56 );
57
58 return filtered.sort((a, b) => {
59 if (sortField === 'name') return a.name.localeCompare(b.name);
60 return b.salary - a.salary;
61 });
62 }, [employees, filter, sortField]);
63 // selectedId n'est PAS dans les deps -> cliquer ne recalcule pas
64
65 return (
66 <div>
67 <input
68 value={filter}
69 onChange={e => setFilter(e.target.value)}
70 placeholder="Filtrer par nom ou departement..."
71 />
72 <p>{visibleEmployees.length} employes affiches sur {employees.length}</p>
73 <table>
74 <thead>
75 <tr>
76 <th onClick={() => setSortField('name')}>Nom</th>
77 <th>Departement</th>
78 <th onClick={() => setSortField('salary')}>Salaire</th>
79 <th>Embauche</th>
80 </tr>
81 </thead>
82 <tbody>
83 {visibleEmployees.map(emp => (
84 <EmployeeRow
85 key={emp.id}
86 employee={emp}
87 isSelected={emp.id === selectedId}
88 onSelect={setSelectedId}
89 />
90 ))}
91 </tbody>
92 </table>
93 </div>
94 );
95}

useMemo cache le RESULTAT, pas la fonction

Distinction essentielle : useMemo execute la fonction et cache ce qu'elle retourne. useCallback, lui, cache la fonction elle-meme sans l'executer. Confondre les deux est une erreur frequente.

useMemo

Cache le resultat du calcul

useMemo(() => expensiveCalc(data), [data])

Retourne : la valeur calculee (ex: un tableau filtre, un objet formate)

useCallback

Cache la fonction elle-meme

useCallback((id) => handleClick(id), [])

Retourne : la reference de la fonction (pas son resultat)

Optimisations·Section 5/10

Pourquoi vos callbacks cassent React.memo ?

useCallback met en cache une definition de fonction entre les renders. Contrairement a useMemo qui cache un resultat, useCallback cache la fonction elle-meme. Son utilite principale : stabiliser les references de fonctions passees en props a des enfants memoises ou utilisees dans les dependances de useEffect.

todo-app.tsxtsx
1// Syntaxe de base : useCallback
2
3import { useCallback, useState } from 'react';
4
5function TodoApp() {
6 const [todos, setTodos] = useState<Todo[]>([]);
7 const [inputValue, setInputValue] = useState('');
8
9 // Sans useCallback : cette fonction est recreee a chaque render
10 // const handleAddTodo = () => { ... };
11
12 // Avec useCallback : la meme reference est reutilisee
13 // tant que les dependances ne changent pas
14 const handleAddTodo = useCallback(() => {
15 if (!inputValue.trim()) return;
16
17 setTodos(prev => [
18 ...prev,
19 { id: crypto.randomUUID(), text: inputValue, done: false },
20 ]);
21 setInputValue('');
22 }, [inputValue]);
23 // ^^^^^^^^^ Depend de inputValue car on le lit dans le callback.
24 // Quand inputValue change, la fonction est recreee avec la nouvelle valeur.
25
26 const handleToggle = useCallback((id: string) => {
27 setTodos(prev =>
28 prev.map(todo =>
29 todo.id === id ? { ...todo, done: !todo.done } : todo
30 )
31 );
32 }, []);
33 // ^^ Pas de dependance : on utilise la forme fonctionnelle de setState
34 // donc on n'a pas besoin de lire "todos" directement.
35 // Cette reference reste stable pour TOUTE la vie du composant.
36
37 const handleDelete = useCallback((id: string) => {
38 setTodos(prev => prev.filter(todo => todo.id !== id));
39 }, []);
40
41 return (
42 <div>
43 <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
44 <button onClick={handleAddTodo}>Ajouter</button>
45 <TodoList
46 todos={todos}
47 onToggle={handleToggle}
48 onDelete={handleDelete}
49 />
50 </div>
51 );
52}

Equivalence avec useMemo

useCallback(fn, deps) est strictement equivalent a useMemo(() => fn, deps). Le hook useCallback est un raccourci syntaxique fourni par React pour un cas d'usage tres courant : memoiser une fonction plutot qu'une valeur.

equivalence.tsxtsx
1// Equivalence useCallback / useMemo
2
3import { useCallback, useMemo } from 'react';
4
5function SearchForm({ onSearch }: { onSearch: (query: string) => void }) {
6 const [query, setQuery] = useState('');
7
8 // Ces deux lignes sont STRICTEMENT equivalentes :
9
10 // Version useCallback (syntaxe dediee, plus lisible)
11 const handleSearch = useCallback(
12 () => onSearch(query),
13 [onSearch, query]
14 );
15
16 // Version useMemo (generique, meme resultat)
17 const handleSearchAlt = useMemo(
18 () => () => onSearch(query), // useMemo retourne la fonction
19 [onSearch, query]
20 );
21
22 // handleSearch === handleSearchAlt (meme comportement)
23 // Preferez useCallback pour les fonctions : plus explicite et lisible.
24
25 return (
26 <div>
27 <input value={query} onChange={e => setQuery(e.target.value)} />
28 <button onClick={handleSearch}>Rechercher</button>
29 </div>
30 );
31}

Sans consommateur memoise, useCallback seul est inutile

useCallback ne sert a rien si la fonction n'est pas passee a un composant enveloppe par React.memo ou utilisee dans un tableau de dependances (useEffect, useMemo). Sans consommateur qui tire parti de la reference stable, vous ajoutez de la complexite sans gain.

  • --useCallback + React.memo = le composant enfant ne se re-rend pas (utile)
  • --useCallback + useEffect deps = l'effet ne se relance pas (utile)
  • --useCallback seul, sans consommateur = overhead sans benefice (inutile)
callback-patterns.tsxtsx
1// ANTI-PATTERN : useCallback sans React.memo
2
3import { useCallback, useState } from 'react';
4
5function ParentBroken() {
6 const [count, setCount] = useState(0);
7
8 // useCallback stabilise la reference de handleClick...
9 const handleClick = useCallback(() => {
10 console.log('clicked');
11 }, []);
12
13 return (
14 <div>
15 <button onClick={() => setCount(c => c + 1)}>
16 Compteur : {count}
17 </button>
18
19 {/* MAIS ChildComponent n'est PAS memoize !
20 Il se re-rend a chaque changement de count,
21 que handleClick soit stable ou non.
22 useCallback ne fait rien d'utile ici. */}
23 <ChildComponent onClick={handleClick} />
24 </div>
25 );
26}
27
28// ChildComponent se re-rend a chaque render du parent
29// car il n'est pas enveloppe par React.memo
30function ChildComponent({ onClick }: { onClick: () => void }) {
31 console.log('ChildComponent render'); // Toujours affiche
32 return <button onClick={onClick}>Action</button>;
33}
34
35// -----------------------------------------------
36
37// PATTERN CORRECT : useCallback + React.memo
38
39import { memo } from 'react';
40
41function ParentFixed() {
42 const [count, setCount] = useState(0);
43
44 const handleClick = useCallback(() => {
45 console.log('clicked');
46 }, []);
47
48 return (
49 <div>
50 <button onClick={() => setCount(c => c + 1)}>
51 Compteur : {count}
52 </button>
53
54 {/* MemoizedChild est enveloppe par memo,
55 et handleClick est une reference stable.
56 MemoizedChild ne se re-rend PAS quand count change. */}
57 <MemoizedChild onClick={handleClick} />
58 </div>
59 );
60}
61
62const MemoizedChild = memo(function MemoizedChild({
63 onClick,
64}: {
65 onClick: () => void;
66}) {
67 console.log('MemoizedChild render'); // Seulement au mount
68 return <button onClick={onClick}>Action</button>;
69});
search-with-effect.tsxtsx
1// useCallback avec useEffect : eviter les boucles infinies
2
3import { useCallback, useEffect, useState } from 'react';
4
5interface SearchResults {
6 items: Array<{ id: string; title: string }>;
7 total: number;
8}
9
10function SearchPage({ apiBase }: { apiBase: string }) {
11 const [query, setQuery] = useState('');
12 const [results, setResults] = useState<SearchResults | null>(null);
13
14 // La fonction de recherche depend de apiBase (prop) et query (state)
15 const fetchResults = useCallback(async () => {
16 if (!query.trim()) {
17 setResults(null);
18 return;
19 }
20
21 const response = await fetch(
22 `${apiBase}/search?q=${encodeURIComponent(query)}`
23 );
24 const data: SearchResults = await response.json();
25 setResults(data);
26 }, [apiBase, query]);
27 // fetchResults est recree UNIQUEMENT quand apiBase ou query changent
28
29 // useEffect depend de fetchResults
30 // Grace a useCallback, cet effet ne se relance que quand
31 // apiBase ou query changent (pas a chaque render)
32 useEffect(() => {
33 const debounceTimer = setTimeout(() => {
34 fetchResults();
35 }, 300);
36
37 return () => clearTimeout(debounceTimer);
38 }, [fetchResults]);
39 // Sans useCallback sur fetchResults :
40 // fetchResults serait une nouvelle reference a chaque render
41 // -> useEffect se relancerait a chaque render
42 // -> requete API a chaque render = boucle infinie potentielle
43
44 return (
45 <div>
46 <input
47 value={query}
48 onChange={e => setQuery(e.target.value)}
49 placeholder="Rechercher..."
50 />
51 {results && (
52 <div>
53 <p>{results.total} resultats</p>
54 <ul>
55 {results.items.map(item => (
56 <li key={item.id}>{item.title}</li>
57 ))}
58 </ul>
59 </div>
60 )}
61 </div>
62 );
63}
Optimisations·Section 6/10

Comment combiner React.memo, useMemo et useCallback ?

Individuellement, chaque outil de memoisation a un role precis. Mais leur veritable puissance se revele quand ils travaillent ensemble. Cette section presente un scenario complet de production ou React.memo, useMemo et useCallback collaborent pour optimiser un tableau de bord avec des interactions frequentes.

Le scenario : un tableau de bord analytique

Imaginons un tableau de bord avec un compteur de notifications en temps reel, une liste de 5 000 transactions filtrable, et un graphique statistique couteux a rendre. L'utilisateur clique frequemment sur le bouton de notifications. Sans memoisation, chaque clic relance le filtrage des 5 000 lignes et le rendu du graphique.

dashboard-sans-memo.tsxtsx
1// VERSION SANS MEMOISATION
2// Chaque clic sur "notifications" re-rend TOUT
3
4import { useState } from 'react';
5
6interface Transaction {
7 id: string;
8 label: string;
9 amount: number;
10 date: string;
11 category: 'income' | 'expense';
12}
13
14// ---------- Composant Parent ----------
15function AnalyticsDashboard() {
16 const [notifications, setNotifications] = useState(0);
17 const [filter, setFilter] = useState<'all' | 'income' | 'expense'>('all');
18 const [transactions] = useState<Transaction[]>(
19 generateTransactions(5000) // 5 000 transactions
20 );
21
22 // PROBLEME 1 : Ce calcul se relance a CHAQUE render
23 // y compris quand on clique sur notifications
24 const filteredTransactions = transactions
25 .filter(t => filter === 'all' || t.category === filter)
26 .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
27 .map(t => ({
28 ...t,
29 displayAmount: new Intl.NumberFormat('fr-FR', {
30 style: 'currency',
31 currency: 'EUR',
32 }).format(t.amount),
33 }));
34 // ~45ms pour 5000 elements
35
36 // PROBLEME 2 : Nouvel objet a chaque render
37 const stats = {
38 total: filteredTransactions.reduce((sum, t) => sum + t.amount, 0),
39 count: filteredTransactions.length,
40 average: filteredTransactions.length > 0
41 ? filteredTransactions.reduce((sum, t) => sum + t.amount, 0) / filteredTransactions.length
42 : 0,
43 };
44
45 // PROBLEME 3 : Nouvelle fonction a chaque render
46 const handleTransactionClick = (id: string) => {
47 console.log('Transaction selectionnee:', id);
48 };
49
50 console.log('Dashboard render - tout se re-execute');
51
52 return (
53 <div>
54 <header>
55 <button onClick={() => setNotifications(n => n + 1)}>
56 Notifications ({notifications})
57 </button>
58 <select
59 value={filter}
60 onChange={e => setFilter(e.target.value as typeof filter)}
61 >
62 <option value="all">Toutes</option>
63 <option value="income">Revenus</option>
64 <option value="expense">Depenses</option>
65 </select>
66 </header>
67
68 {/* Tout se re-rend a chaque clic sur notifications */}
69 <StatsChart stats={stats} />
70 <TransactionList
71 transactions={filteredTransactions}
72 onItemClick={handleTransactionClick}
73 />
74 </div>
75 );
76}
77
78// Ces composants se re-rendent systematiquement
79function StatsChart({ stats }: { stats: { total: number; count: number; average: number } }) {
80 console.log('StatsChart render - rendu SVG couteux...');
81 // Rendu d'un graphique SVG complexe (~30ms)
82 return <div>Graphique : {stats.count} transactions</div>;
83}
84
85function TransactionList({
86 transactions,
87 onItemClick,
88}: {
89 transactions: Array<Transaction & { displayAmount: string }>;
90 onItemClick: (id: string) => void;
91}) {
92 console.log('TransactionList render -', transactions.length, 'lignes');
93 return (
94 <ul>
95 {transactions.map(t => (
96 <li key={t.id} onClick={() => onItemClick(t.id)}>
97 {t.label} : {t.displayAmount}
98 </li>
99 ))}
100 </ul>
101 );
102}
103
104// Console apres clic sur Notifications :
105// Dashboard render - tout se re-execute (~45ms calcul)
106// StatsChart render - rendu SVG couteux... (~30ms rendu)
107// TransactionList render - 5000 lignes (~20ms rendu)
108// TOTAL : ~95ms par clic = lag perceptible
dashboard-avec-memo.tsxtsx
1// VERSION AVEC MEMOISATION
2// Chaque outil a un role precis dans l'optimisation
3
4import { memo, useCallback, useMemo, useState } from 'react';
5
6interface Transaction {
7 id: string;
8 label: string;
9 amount: number;
10 date: string;
11 category: 'income' | 'expense';
12}
13
14// ---------- Composant Parent ----------
15function AnalyticsDashboard() {
16 const [notifications, setNotifications] = useState(0);
17 const [filter, setFilter] = useState<'all' | 'income' | 'expense'>('all');
18 const [transactions] = useState<Transaction[]>(
19 generateTransactions(5000)
20 );
21
22 // SOLUTION 1 : useMemo pour le calcul couteux
23 // Ne recalcule QUE quand transactions ou filter changent
24 // Cliquer sur notifications ne relance PAS ce calcul
25 const filteredTransactions = useMemo(() => {
26 console.log('Recalcul filteredTransactions...');
27 return transactions
28 .filter(t => filter === 'all' || t.category === filter)
29 .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
30 .map(t => ({
31 ...t,
32 displayAmount: new Intl.NumberFormat('fr-FR', {
33 style: 'currency',
34 currency: 'EUR',
35 }).format(t.amount),
36 }));
37 }, [transactions, filter]);
38
39 // SOLUTION 2 : useMemo pour stabiliser la reference objet
40 // stats est derive de filteredTransactions
41 // Meme reference tant que filteredTransactions ne change pas
42 const stats = useMemo(() => ({
43 total: filteredTransactions.reduce((sum, t) => sum + t.amount, 0),
44 count: filteredTransactions.length,
45 average: filteredTransactions.length > 0
46 ? filteredTransactions.reduce((sum, t) => sum + t.amount, 0) / filteredTransactions.length
47 : 0,
48 }), [filteredTransactions]);
49
50 // SOLUTION 3 : useCallback pour stabiliser la reference fonction
51 // handleTransactionClick garde la meme reference entre les renders
52 const handleTransactionClick = useCallback((id: string) => {
53 console.log('Transaction selectionnee:', id);
54 }, []);
55
56 console.log('Dashboard render');
57
58 return (
59 <div>
60 <header>
61 <button onClick={() => setNotifications(n => n + 1)}>
62 Notifications ({notifications})
63 </button>
64 <select
65 value={filter}
66 onChange={e => setFilter(e.target.value as typeof filter)}
67 >
68 <option value="all">Toutes</option>
69 <option value="income">Revenus</option>
70 <option value="expense">Depenses</option>
71 </select>
72 </header>
73
74 {/* React.memo empeche le re-render si les props sont stables */}
75 <MemoizedStatsChart stats={stats} />
76 <MemoizedTransactionList
77 transactions={filteredTransactions}
78 onItemClick={handleTransactionClick}
79 />
80 </div>
81 );
82}
83
84// SOLUTION 4 : React.memo sur les composants enfants
85// Ne se re-rend que si stats change (reference stable grace a useMemo)
86const MemoizedStatsChart = memo(function StatsChart({
87 stats,
88}: {
89 stats: { total: number; count: number; average: number };
90}) {
91 console.log('StatsChart render');
92 return <div>Graphique : {stats.count} transactions</div>;
93});
94
95// Ne se re-rend que si transactions ou onItemClick changent
96const MemoizedTransactionList = memo(function TransactionList({
97 transactions,
98 onItemClick,
99}: {
100 transactions: Array<Transaction & { displayAmount: string }>;
101 onItemClick: (id: string) => void;
102}) {
103 console.log('TransactionList render -', transactions.length, 'lignes');
104 return (
105 <ul>
106 {transactions.map(t => (
107 <li key={t.id} onClick={() => onItemClick(t.id)}>
108 {t.label} : {t.displayAmount}
109 </li>
110 ))}
111 </ul>
112 );
113});
114
115// Console apres clic sur Notifications :
116// Dashboard render (~0.3ms)
117//
118// C'est TOUT. StatsChart et TransactionList ne se re-rendent PAS.
119// useMemo retourne les valeurs en cache, useCallback la meme reference.
120// React.memo compare les props : identiques -> pas de re-render.
121//
122// Console apres changement de filtre :
123// Dashboard render
124// Recalcul filteredTransactions... (~45ms, necessaire)
125// StatsChart render (~30ms, necessaire)
126// TransactionList render - 3200 lignes (~15ms, necessaire)
127// Les re-renders sont limites aux cas ou les donnees changent reellement.

Trace d'execution : que se passe-t-il exactement ?

Quand l'utilisateur clique sur 'Notifications', voici le cheminement precis de React a travers le code optimise.

Etape 1 : setState declenche le render

setNotifications(n => n + 1) marque AnalyticsDashboard pour re-render. React re-execute la fonction composant.

Etape 2 : useMemo verifie ses dependances

[transactions, filter] n'ont pas change (notifications n'est pas dans les deps). useMemo retourne le resultat en cache sans recalculer. Meme chose pour stats.

Etape 3 : useCallback retourne la meme reference

Les dependances de handleTransactionClick ([]) n'ont pas change. useCallback retourne la meme reference de fonction.

Etape 4 : React.memo compare les props

MemoizedStatsChart : stats est la meme reference -> pas de re-render. MemoizedTransactionList : transactions et onItemClick sont les memes references -> pas de re-render. Seul le header se met a jour pour afficher le nouveau compteur.

Comparaison des performances

Sur ce scenario avec 5 000 transactions, le gain est significatif. Sans memoisation, chaque clic sur le bouton de notifications coute environ 95ms (calcul + rendu graphique + rendu liste). Avec memoisation, le meme clic coute environ 0,3ms : seul le composant parent se re-execute, et les enfants sont sautes. C'est un facteur d'amelioration de plus de 300x sur cette interaction specifique.

Bonnes Pratiques·Section 7/10

React.memo vs useMemo vs useCallback : lequel utiliser ?

Les trois outils de memoisation React repondent a des besoins differents mais complementaires. Ce tableau comparatif synthetise leurs forces, limites et cas d'usage pour vous aider a choisir le bon outil selon le contexte.

React.memo
HOC qui empeche le re-render si les props sont identiques (shallow comparison par defaut).
Avantages
  • Evite les re-renders couteux des composants enfants
  • Simple a appliquer (envelopper le composant)
  • Fonctionne avec les composants fonctionnels et classes
Inconvenients
  • Shallow comparison par defaut (objets/fonctions inline contournent le memo)
  • Inutile si les props changent a chaque render
  • Overhead de comparaison des props a chaque render du parent
Cas d'usage
  • Composants lourds en rendu (graphiques, listes longues)
  • Listes avec beaucoup d'items (chaque item memoize)
  • Composants recevant des props stables (primitives, refs useMemo/useCallback)
useMemo
Hook qui cache le resultat d'un calcul entre les renders. Re-execute uniquement quand les dependances changent.
Avantages
  • Evite les recalculs couteux (filtrage, tri, formatage)
  • Stabilise les references d'objets pour React.memo
  • Type-safe avec TypeScript (inference automatique)
Inconvenients
  • Consomme de la memoire (cache le resultat precedent)
  • Comparaison des dependances a chaque render
  • Peut masquer des problemes d'architecture (state derive mal concu)
Cas d'usage
  • Calculs complexes : filtrage, tri, aggregation de grandes listes
  • Derived state : valeur calculee a partir de props ou state
  • Stabiliser les props objet pour que React.memo fonctionne
useCallback
Hook qui cache une definition de fonction entre les renders. La reference reste stable tant que les dependances ne changent pas.
Avantages
  • Stabilise les references de fonctions pour React.memo
  • Evite les re-renders inutiles des enfants memoises
  • Essentiel pour les dependances de useEffect
Inconvenients
  • Inutile sans consommateur memoize (React.memo ou useEffect)
  • Closure stale si les dependances sont incorrectes
  • Overhead de memoisation (comparaison des deps)
Cas d'usage
  • Callbacks passes a des enfants enveloppes par React.memo
  • Fonctions dans les dependances de useEffect
  • Event handlers stables pour des composants optimises

Regles pratiques : quand utiliser quoi

Un guide de decision rapide pour choisir le bon outil de memoisation selon votre situation.

Utilisez React.memo quand...

  • Le composant est couteux a rendre (graphique, longue liste, SVG complexe)
  • Le composant recoit souvent les memes props
  • Le parent se re-rend frequemment pour des raisons sans rapport avec l'enfant

Utilisez useMemo quand...

  • Un calcul prend plus de quelques millisecondes (mesure avec le Profiler)
  • Vous devez stabiliser une reference objet pour un enfant memoise
  • Vous derivez des donnees complexes a partir de state ou props

Utilisez useCallback quand...

  • La fonction est passee en prop a un enfant enveloppe par React.memo
  • La fonction est dans le tableau de dependances d'un useEffect
  • La fonction est passee a un hook tiers qui compare les references

Ne memoisez PAS quand...

  • Le composant est leger et rapide a rendre (< 1ms)
  • Les props changent presque a chaque render
  • Vous n'avez pas mesure de probleme de performance reel
  • Le composant est un noeud feuille sans enfants complexes

La chaine de memoisation

Pour que la memoisation soit efficace, chaque maillon de la chaine doit etre en place. Un seul maillon manquant et toute l'optimisation est perdue.

Maillon 1 : Le composant enfant est enveloppe par React.memo

+

Maillon 2 : Les props objet sont stabilisees par useMemo

+

Maillon 3 : Les props fonction sont stabilisees par useCallback

=

Resultat : le composant enfant ne se re-rend que lorsque ses donnees changent reellement.

Testez ces strategies en liveInteractif

Comparez React.memo, useMemo et useCallback avec des mesures reelles de temps de rendu sur un benchmark de liste de produits.

Bonnes Pratiques·Section 8/10

Quelles erreurs éviter avec la mémoisation ?

La memoisation est un outil puissant, mais mal utilisee, elle peut degrader les performances, introduire des bugs subtils ou simplement ajouter de la complexite sans aucun benefice. Voici les quatre erreurs les plus frequentes, avec pour chacune le code problematique et la correction.

Erreur 1 : Tout memoiser par defaut

Envelopper chaque composant avec React.memo et chaque valeur avec useMemo est un reflexe courant mais contre-productif. Chaque memoisation a un cout : comparaison des dependances, memoire pour le cache, complexite du code. Si le composant est leger ou si les props changent a chaque render, ce cout depasse le benefice.

over-memoization.tsxtsx
1// ANTI-PATTERN : tout memoiser sans raison
2
3import { memo, useCallback, useMemo } from 'react';
4
5// Memoiser un composant trivial : aucun benefice
6const Label = memo(function Label({ text }: { text: string }) {
7 // Ce composant rend un seul <span> : cout de render < 0.01ms
8 // Le cout de comparaison des props par memo est du meme ordre
9 return <span className="text-sm text-gray-600">{text}</span>;
10});
11
12function FormField({ label, value, onChange }: FormFieldProps) {
13 // Memoiser un calcul trivial : plus couteux que le calcul lui-meme
14 const trimmedValue = useMemo(() => value.trim(), [value]);
15 // value.trim() prend ~0.001ms
16 // La comparaison des deps de useMemo prend ~0.001ms aussi
17 // Aucun gain, mais code plus complexe
18
19 // useCallback sans consommateur memoise : overhead pur
20 const handleChange = useCallback(
21 (e: React.ChangeEvent<HTMLInputElement>) => {
22 onChange(e.target.value);
23 },
24 [onChange]
25 );
26 // handleChange est passe a <input>, un element natif
27 // Les elements natifs ne beneficient PAS de React.memo
28 // Cette memoisation ne sert strictement a rien
29
30 return (
31 <div>
32 <Label text={label} />
33 <input value={trimmedValue} onChange={handleChange} />
34 </div>
35 );
36}
correct-approach.tsxtsx
1// CORRECTION : memoiser uniquement ce qui en a besoin
2
3function FormField({ label, value, onChange }: FormFieldProps) {
4 // Pas de useMemo : value.trim() est instantane
5 const trimmedValue = value.trim();
6
7 // Pas de useCallback : <input> est un element natif
8 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
9 onChange(e.target.value);
10 };
11
12 return (
13 <div>
14 {/* Pas de memo : Label est trivial */}
15 <span className="text-sm text-gray-600">{label}</span>
16 <input value={trimmedValue} onChange={handleChange} />
17 </div>
18 );
19}
20
21// Regle : ne memoiser que lorsque le Profiler montre un probleme
22// ou quand un enfant React.memo a besoin de references stables

Erreur 2 : Oublier des dependances (stale closures)

Quand une fonction memorisee par useCallback ou useMemo lit une variable mais ne la liste pas dans ses dependances, elle capture une valeur obsolete. C'est le bug de la "stale closure" : la fonction fonctionne avec des donnees du passe.

stale-closure-bug.tsxtsx
1// BUG : stale closure - la fonction utilise une valeur obsolete
2
3import { useCallback, useState } from 'react';
4
5function Chat() {
6 const [messages, setMessages] = useState<string[]>([]);
7 const [draft, setDraft] = useState('');
8
9 // BUG : draft n'est PAS dans les dependances
10 const handleSend = useCallback(() => {
11 if (!draft.trim()) return;
12
13 // draft est capture au moment de la creation du callback
14 // Si l'utilisateur tape "Bonjour" puis "Comment ca va",
15 // handleSend enverra toujours la valeur de draft au premier render
16 setMessages(prev => [...prev, draft]); // draft est toujours ''
17 setDraft('');
18 }, []); // <-- PROBLEME : deps vides, draft n'est jamais mis a jour
19
20 return (
21 <div>
22 <ul>
23 {messages.map((msg, i) => <li key={i}>{msg}</li>)}
24 </ul>
25 <input value={draft} onChange={e => setDraft(e.target.value)} />
26 <button onClick={handleSend}>Envoyer</button>
27 </div>
28 );
29}
30
31// Execution pas a pas :
32// 1. Render initial : draft = '', handleSend capture draft = ''
33// 2. L'utilisateur tape "Bonjour" : draft = 'Bonjour'
34// 3. handleSend est TOUJOURS la version du render initial
35// 4. L'utilisateur clique sur Envoyer
36// 5. handleSend lit draft... qui vaut '' (la valeur capturee)
37// 6. Le message envoye est vide !
stale-closure-fix.tsxtsx
1// CORRECTION : inclure draft dans les dependances
2
3function Chat() {
4 const [messages, setMessages] = useState<string[]>([]);
5 const [draft, setDraft] = useState('');
6
7 // FIX 1 : Ajouter draft aux dependances
8 const handleSend = useCallback(() => {
9 if (!draft.trim()) return;
10 setMessages(prev => [...prev, draft]); // draft est a jour
11 setDraft('');
12 }, [draft]); // <-- draft dans les deps : la fonction se recree quand draft change
13
14 // FIX 2 (alternative) : utiliser un ref pour les valeurs qui changent souvent
15 // Utile quand vous voulez une reference stable ET une valeur a jour
16 const draftRef = useRef(draft);
17 draftRef.current = draft; // Mis a jour a chaque render
18
19 const handleSendStable = useCallback(() => {
20 if (!draftRef.current.trim()) return;
21 setMessages(prev => [...prev, draftRef.current]);
22 setDraft('');
23 }, []); // Pas besoin de draft dans les deps : on lit la ref
24
25 return (
26 <div>
27 <ul>
28 {messages.map((msg, i) => <li key={i}>{msg}</li>)}
29 </ul>
30 <input value={draft} onChange={e => setDraft(e.target.value)} />
31 <button onClick={handleSend}>Envoyer</button>
32 </div>
33 );
34}

Erreur 3 : React.memo sans stabiliser les props

Envelopper un composant avec React.memo mais continuer a lui passer des objets et fonctions inline en props est l'une des erreurs les plus frequentes. Le memo est contourne a chaque render car les references changent. Vous payez le cout de la comparaison sans aucun benefice.

memo-bypassed.tsxtsx
1// PROBLEME : memo contourne par des props inline
2
3import { memo, useState } from 'react';
4
5// Le composant EST memoize...
6const ExpensiveList = memo(function ExpensiveList({
7 items,
8 config,
9 onItemClick,
10}: {
11 items: string[];
12 config: { pageSize: number; showHeader: boolean };
13 onItemClick: (item: string) => void;
14}) {
15 console.log('ExpensiveList render -', items.length, 'items');
16 return (
17 <ul>
18 {items.map(item => (
19 <li key={item} onClick={() => onItemClick(item)}>{item}</li>
20 ))}
21 </ul>
22 );
23});
24
25function PageBroken() {
26 const [theme, setTheme] = useState('light');
27 const items = ['React', 'Vue', 'Angular', 'Svelte'];
28
29 return (
30 <div>
31 <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
32 Theme : {theme}
33 </button>
34
35 <ExpensiveList
36 items={items}
37 // Nouvel objet a chaque render -> memo contourne
38 config={{ pageSize: 20, showHeader: true }}
39 // Nouvelle fonction a chaque render -> memo contourne
40 onItemClick={(item) => console.log('Clic:', item)}
41 />
42 {/* Resultat : ExpensiveList se re-rend a chaque
43 changement de theme malgre React.memo */}
44 </div>
45 );
46}
memo-working.tsxtsx
1// SOLUTION : stabiliser les references avec useMemo et useCallback
2
3import { memo, useCallback, useMemo, useState } from 'react';
4
5function PageFixed() {
6 const [theme, setTheme] = useState('light');
7 const items = ['React', 'Vue', 'Angular', 'Svelte'];
8
9 // useMemo stabilise la reference de l'objet config
10 const config = useMemo(
11 () => ({ pageSize: 20, showHeader: true }),
12 [] // Pas de dependances = reference stable
13 );
14
15 // useCallback stabilise la reference de la fonction
16 const handleItemClick = useCallback((item: string) => {
17 console.log('Clic:', item);
18 }, []);
19
20 return (
21 <div>
22 <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
23 Theme : {theme}
24 </button>
25
26 <ExpensiveList
27 items={items}
28 config={config} // Meme reference entre les renders
29 onItemClick={handleItemClick} // Meme reference entre les renders
30 />
31 {/* Resultat : ExpensiveList ne se re-rend PAS
32 quand le theme change */}
33 </div>
34 );
35}

Erreur 4 : useMemo pour des calculs triviaux

Utiliser useMemo pour des operations qui prennent quelques microsecondes (addition, concatenation, acces a une propriete) ajoute de la complexite sans benefice mesurable. Le cout de useMemo (comparaison des dependances, stockage en memoire) est du meme ordre que le calcul lui-meme.

trivial-memo.tsxtsx
1// ANTI-PATTERN : useMemo pour des calculs triviaux
2
3function UserGreeting({ firstName, lastName, age }: UserProps) {
4 // Inutile : la concatenation prend ~0.001ms
5 const fullName = useMemo(
6 () => `${firstName} ${lastName}`,
7 [firstName, lastName]
8 );
9
10 // Inutile : une addition prend ~0.000001ms
11 const birthYear = useMemo(
12 () => new Date().getFullYear() - age,
13 [age]
14 );
15
16 // Inutile : un acces a propriete prend ~0.0001ms
17 const isAdult = useMemo(() => age >= 18, [age]);
18
19 return (
20 <div>
21 <h2>{fullName}</h2>
22 <p>Ne en {birthYear}</p>
23 {isAdult && <span>Majeur</span>}
24 </div>
25 );
26}
27
28// CORRECTION : calcul direct, simple et lisible
29
30function UserGreeting({ firstName, lastName, age }: UserProps) {
31 const fullName = `${firstName} ${lastName}`;
32 const birthYear = new Date().getFullYear() - age;
33 const isAdult = age >= 18;
34
35 return (
36 <div>
37 <h2>{fullName}</h2>
38 <p>Ne en {birthYear}</p>
39 {isAdult && <span>Majeur</span>}
40 </div>
41 );
42}
43
44// Regle : reservez useMemo aux calculs qui prennent > 1ms
45// ou aux objets qui doivent avoir une reference stable

Resume des 4 erreurs a eviter

Gardez ces regles en tete pour utiliser la memoisation de maniere efficace et pertinente.

1. Tout memoiser par defaut

Chaque memoisation a un cout. Ne memoiser que ce que le Profiler identifie comme problematique.

2. Oublier des dependances

Les stale closures causent des bugs subtils. Activez le plugin ESLint exhaustive-deps.

3. memo sans stabiliser les props

React.memo est inutile si les objets et fonctions sont crees inline dans le parent.

4. useMemo pour des calculs triviaux

Reservez useMemo aux calculs > 1ms ou aux references qui doivent rester stables.

Avance·Section 9/10

Dans quels cas la mémoisation est contre-productive ?

La memoisation n'est pas une optimisation gratuite. Elle a un cout en memoire, en complexite du code et en temps de comparaison des dependances. Dans de nombreux cas, ne PAS memoiser est la meilleure decision. Cette section etablit des regles concretes pour savoir quand la memoisation est contre-productive.

Regles de decision : faut-il memoiser ?

Avant d'ajouter React.memo, useMemo ou useCallback, passez par cette checklist.

1. Le composant est leger (leaf node)

Un composant qui rend quelques elements HTML sans enfants complexes se re-render en moins de 0.1ms. Le cout de React.memo (comparaison des props) est du meme ordre. Pas de memoisation.

2. Les props changent a chaque render

Si les props changent systematiquement (position dans une animation, timestamp, objet derive du state parent), React.memo compare et laisse passer a chaque fois. Vous payez le cout de comparaison sans aucun skip.

3. Pas de profiling prealable

Si vous n'avez pas mesure un probleme de performance avec le React DevTools Profiler, vous optimisez a l'aveugle. La memoisation prematuree ajoute de la complexite sans benefice prouve.

4. Le calcul est trivial

Les operations qui prennent moins de 1ms (concatenation, acces propriete, arithmetique simple) ne justifient pas useMemo. La comparaison des dependances coute autant que le calcul.

Exemple : quand la memoisation AJOUTE de la complexite

Voici un composant ou chaque memoisation est inutile. Le code est plus long, plus difficile a lire, et les performances sont identiques (voire legerement moins bonnes a cause de l'overhead).

over-memoized.tsxtsx
1// ANTI-PATTERN : memoisation excessive sur un composant simple
2
3import { memo, useMemo, useCallback, useState } from 'react';
4
5// memo inutile : le composant est un leaf node trivial
6const StatusBadge = memo(function StatusBadge({ active }: { active: boolean }) {
7 return (
8 <span className={active ? 'text-green-600' : 'text-red-600'}>
9 {active ? 'Actif' : 'Inactif'}
10 </span>
11 );
12});
13
14function UserCard({ user }: { user: User }) {
15 const [isEditing, setIsEditing] = useState(false);
16
17 // useMemo inutile : concatenation instantanee
18 const fullName = useMemo(
19 () => `${user.firstName} ${user.lastName}`,
20 [user.firstName, user.lastName]
21 );
22
23 // useMemo inutile : acces propriete
24 const initials = useMemo(
25 () => user.firstName[0] + user.lastName[0],
26 [user.firstName, user.lastName]
27 );
28
29 // useCallback inutile : passe a un element natif <button>
30 // Les elements natifs ne sont pas wrapes par React.memo
31 const handleEdit = useCallback(() => {
32 setIsEditing(true);
33 }, []);
34
35 // useCallback inutile : passe a un element natif <button>
36 const handleCancel = useCallback(() => {
37 setIsEditing(false);
38 }, []);
39
40 return (
41 <div className="p-4 border rounded-lg">
42 <div className="flex items-center gap-3">
43 <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
44 {initials}
45 </div>
46 <div>
47 <h3 className="font-medium">{fullName}</h3>
48 <StatusBadge active={user.isActive} />
49 </div>
50 </div>
51 <div className="mt-3">
52 {isEditing ? (
53 <button onClick={handleCancel}>Annuler</button>
54 ) : (
55 <button onClick={handleEdit}>Modifier</button>
56 )}
57 </div>
58 </div>
59 );
60}
clean-version.tsxtsx
1// VERSION CORRECTE : simple, lisible, performante
2
3function StatusBadge({ active }: { active: boolean }) {
4 return (
5 <span className={active ? 'text-green-600' : 'text-red-600'}>
6 {active ? 'Actif' : 'Inactif'}
7 </span>
8 );
9}
10
11function UserCard({ user }: { user: User }) {
12 const [isEditing, setIsEditing] = useState(false);
13
14 // Calculs directs : instantanes, pas besoin de cache
15 const fullName = `${user.firstName} ${user.lastName}`;
16 const initials = user.firstName[0] + user.lastName[0];
17
18 return (
19 <div className="p-4 border rounded-lg">
20 <div className="flex items-center gap-3">
21 <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
22 {initials}
23 </div>
24 <div>
25 <h3 className="font-medium">{fullName}</h3>
26 <StatusBadge active={user.isActive} />
27 </div>
28 </div>
29 <div className="mt-3">
30 {isEditing ? (
31 <button onClick={() => setIsEditing(false)}>Annuler</button>
32 ) : (
33 <button onClick={() => setIsEditing(true)}>Modifier</button>
34 )}
35 </div>
36 </div>
37 );
38}
39
40// Resultat : meme performance, 30% de code en moins, plus lisible

La regle d'or : mesurer d'abord, memoiser ensuite

Le React DevTools Profiler est l'outil qui distingue les optimisations utiles des optimisations prematurees. Il mesure le temps reel de chaque render et identifie les composants qui ralentissent votre application. Sans cette mesure, toute memoisation est une supposition.

profiling-workflow.tstypescript
1// Workflow de profiling pour decider de la memoisation
2
3// Etape 1 : Installer les React DevTools (extension navigateur)
4// Onglet "Profiler" dans les DevTools React
5
6// Etape 2 : Enregistrer une session
7// - Cliquer sur "Record"
8// - Effectuer l'interaction a analyser (clic, saisie, navigation)
9// - Cliquer sur "Stop"
10
11// Etape 3 : Analyser les flamegraphs
12// Le Profiler affiche :
13// - Chaque composant qui a re-render
14// - Le temps de render en millisecondes
15// - La raison du re-render (props changed, state changed, hooks changed)
16
17// Etape 4 : Identifier les candidats a la memoisation
18// Chercher les composants qui :
19// ✓ Se re-rendent frequemment (a chaque keystroke, scroll, etc.)
20// ✓ Prennent > 1ms a re-render
21// ✓ N'ont pas besoin de re-render (props inchangees)
22
23// Etape 5 : Appliquer la memoisation ciblee
24// UNIQUEMENT sur les composants identifies en etape 4
25
26// Exemple concret de profiling :
27// Avant : <ProductList> re-render en 15ms a chaque keystroke du search
28// Analyse : ProductList recoit une prop onSelect recree a chaque render
29// Fix : useCallback sur onSelect + React.memo sur ProductList
30// Apres : <ProductList> skip le re-render, search fluide
31
32// Etape 6 : Re-profiler pour verifier
33// Confirmer que la memoisation a bien reduit le temps de render
34// Si pas d'amelioration mesurable : retirer la memoisation
35
36// Le "Highlight updates" dans les React DevTools
37// est aussi utile pour visualiser les re-renders en temps reel.
38// Components > Settings > "Highlight updates when components render"
39// Les composants qui re-rendent s'entourent d'une bordure coloree.

Quand NE PAS memoiser : resume

Gardez ces situations en tete pour eviter la memoisation inutile.

Ne pas memoiser quand...

  • -- Le composant est un leaf node simple
  • -- Les props changent a chaque render
  • -- Le calcul prend moins de 1ms
  • -- Le callback est passe a un element natif
  • -- Aucun profiling n'a ete fait
  • -- Le composant enfant n'est pas wrape par React.memo

Memoiser quand...

  • -- Le Profiler montre un re-render couteux (> 1ms)
  • -- Le composant enfant est wrape par React.memo
  • -- Le calcul traite > 100 elements
  • -- La valeur est utilisee comme dependance de useEffect
  • -- Le re-render cause un lag perceptible par l'utilisateur
  • -- L'objet/fonction doit avoir une reference stable

Principe directeur

"Ecrivez d'abord du code lisible et simple. Mesurez les performances avec le Profiler. Memoiser uniquement la ou le Profiler montre un probleme. Re-mesurez pour confirmer le gain. Si le gain n'est pas mesurable, retirez la memoisation."

Avance·Section 10/10

Le React Compiler va-t-il remplacer la mémoisation manuelle ?

Le React Compiler (anciennement React Forget) est un compilateur qui transforme automatiquement votre code React pour ajouter la memoisation optimale. Il analyse les dependances de chaque composant au build time et insere les equivalents de useMemo, useCallback et React.memo la ou c'est necessaire. L'objectif : ecrire du code React naturel et laisser le compilateur gerer les optimisations.

Avant/apres : le meme composant

Voici un composant tel que vous l'ecrivez, puis ce que le compilateur genere en sortie. Le code source reste simple et lisible, le compilateur gere l'optimisation.

source-code.tsxtsx
1// CE QUE VOUS ECRIVEZ (code source)
2
3function ProductPage({ products, category }: ProductPageProps) {
4 const [search, setSearch] = useState('');
5
6 // Calcul derive : filtre les produits
7 const filteredProducts = products
8 .filter(p => p.category === category)
9 .filter(p => p.name.toLowerCase().includes(search.toLowerCase()));
10
11 // Callback passe a un enfant
12 const handleSelect = (id: string) => {
13 router.push(`/products/${id}`);
14 };
15
16 return (
17 <div>
18 <SearchBar value={search} onChange={setSearch} />
19 <ProductList
20 products={filteredProducts}
21 onSelect={handleSelect}
22 />
23 <ProductCount count={filteredProducts.length} />
24 </div>
25 );
26}
27
28// Aucun useMemo, aucun useCallback, aucun React.memo
29// Le code est naturel, lisible et maintenable
compiler-output.tsxtsx
1// CE QUE LE COMPILATEUR GENERE (output simplifie)
2
3function ProductPage({ products, category }: ProductPageProps) {
4 const [search, setSearch] = useState('');
5
6 // Le compilateur detecte que filteredProducts depend de
7 // products, category et search.
8 // Il insere l'equivalent d'un useMemo automatiquement.
9 const filteredProducts = useMemo(
10 () => products
11 .filter(p => p.category === category)
12 .filter(p => p.name.toLowerCase().includes(search.toLowerCase())),
13 [products, category, search]
14 );
15
16 // Le compilateur detecte que handleSelect ne depend
17 // d'aucune variable reactive (router est stable).
18 // Il insere l'equivalent d'un useCallback.
19 const handleSelect = useCallback(
20 (id: string) => {
21 router.push(`/products/${id}`);
22 },
23 [] // aucune dependance reactive
24 );
25
26 return (
27 <div>
28 <SearchBar value={search} onChange={setSearch} />
29 <ProductList
30 products={filteredProducts}
31 onSelect={handleSelect}
32 />
33 <ProductCount count={filteredProducts.length} />
34 </div>
35 );
36}
37
38// Le compilateur a aussi wrape les composants enfants
39// avec l'equivalent de React.memo la ou c'est pertinent.
40// Tout cela est transparent : vous ne voyez que le code source.

Ce que le compilateur fait vs ce qu'il ne fait pas

Ce que le compilateur FAIT
Optimisations automatiques inserees au build time par le React Compiler
Avantages
  • Memoisation des valeurs derivees (equivalent useMemo)
  • Memoisation des callbacks (equivalent useCallback)
  • Memoisation des composants (equivalent React.memo)
  • Analyse statique des dependances (jamais de stale closure)
  • Granularite fine : memoisation au niveau expression, pas composant
  • Compatible avec le code React existant sans modification
Inconvenients
    Cas d'usage
    • Tout projet React 19+ avec un build step
    • Migration progressive depuis un code avec memoisation manuelle
    Ce que le compilateur NE FAIT PAS
    Limites et cas ou l'intervention manuelle reste necessaire
    Avantages
      Inconvenients
      • Pas d'optimisation algorithmique (O(n^2) reste O(n^2))
      • Pas de virtualisation (utiliser TanStack Virtual)
      • Pas de code splitting (utiliser React.lazy)
      • Pas de restructuration de composants (extraire les composants reste votre travail)
      • Pas de gestion du state serveur (utiliser TanStack Query)
      • Ne fonctionne pas avec des patterns non-standard (mutation directe, eval)
      Cas d'usage
      • Cas ou le profiling montre un probleme non lie au re-render
      • Optimisations architecturales (lazy loading, virtualisation)

      Activer le React Compiler

      compiler-setup.tstypescript
      1// Installation dans un projet Next.js 16+
      2// Le compilateur est inclus dans React 19
      3
      4// next.config.ts
      5import type { NextConfig } from 'next';
      6
      7const nextConfig: NextConfig = {
      8 experimental: {
      9 reactCompiler: true,
      10 },
      11};
      12
      13export default nextConfig;
      14
      15// Pour un projet Vite / Webpack classique :
      16// npm install babel-plugin-react-compiler
      17
      18// babel.config.js
      19module.exports = {
      20 plugins: [
      21 ['babel-plugin-react-compiler', {
      22 // Options du compilateur
      23 target: '19', // version React cible
      24 }],
      25 ],
      26};
      27
      28// Verification : le compilateur log les composants optimises
      29// dans la console pendant le build.
      30// Si un composant ne peut pas etre optimise, un warning est emis.

      Les regles du compilateur

      Le React Compiler applique les "Rules of React" de maniere stricte. Si votre code les enfreint, le compilateur ne peut pas optimiser le composant concerne. Voici les regles qui importent le plus pour la compatibilite.

      compiler-rules.tsxtsx
      1// REGLE 1 : Les composants et hooks doivent etre purs
      2// Le compilateur suppose que le rendu est deterministe
      3
      4// COMPATIBLE : pas d'effet de bord pendant le render
      5function GoodComponent({ items }: { items: Item[] }) {
      6 const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
      7 return <ul>{sorted.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
      8}
      9
      10// INCOMPATIBLE : mutation pendant le render
      11function BadComponent({ items }: { items: Item[] }) {
      12 items.sort((a, b) => a.name.localeCompare(b.name)); // Mutation du prop !
      13 return <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
      14}
      15
      16
      17// REGLE 2 : Les props et state sont immutables
      18// Toujours creer de nouveaux objets, jamais muter
      19
      20// COMPATIBLE
      21function handleAdd(item: Item) {
      22 setItems(prev => [...prev, item]); // Nouveau tableau
      23}
      24
      25// INCOMPATIBLE
      26function handleAddBad(item: Item) {
      27 items.push(item); // Mutation directe
      28 setItems(items); // Meme reference, React ne detecte pas le changement
      29}
      30
      31
      32// REGLE 3 : Les valeurs de retour et arguments des hooks sont immutables
      33// Ne pas muter ce que useState ou useReducer retourne
      34
      35// COMPATIBLE
      36const [user, setUser] = useState({ name: 'Alice', age: 30 });
      37setUser({ ...user, age: 31 }); // Nouvel objet
      38
      39// INCOMPATIBLE
      40user.age = 31; // Mutation directe de l'etat
      41setUser(user); // Meme reference
      42
      43
      44// REGLE 4 : Pas de lecture de valeurs pendant le render
      45// qui changent entre les renders sans etre des props/state
      46
      47// COMPATIBLE
      48function Timer() {
      49 const [now, setNow] = useState(Date.now());
      50 useEffect(() => {
      51 const id = setInterval(() => setNow(Date.now()), 1000);
      52 return () => clearInterval(id);
      53 }, []);
      54 return <span>{new Date(now).toLocaleTimeString()}</span>;
      55}
      56
      57// INCOMPATIBLE : lit Date.now() pendant le render
      58function BadTimer() {
      59 return <span>{new Date(Date.now()).toLocaleTimeString()}</span>;
      60 // Le compilateur ne peut pas savoir quand invalider le cache
      61}

      L'avenir de la memoisation React

      Comment le paysage de l'optimisation React evolue avec le compilateur.

      Aujourd'hui : transition progressive

      Le React Compiler est stable et utilisable en production depuis React 19. La memoisation manuelle reste valide et coexiste avec le compilateur. Les projets existants peuvent migrer progressivement.

      Court terme : moins de code manuel

      Les nouveaux projets n'ont plus besoin d'ecrire useMemo/useCallback/React.memo. Le code source devient plus simple et plus lisible. Le plugin ESLint react-compiler aide a corriger les patterns incompatibles.

      Long terme : memoisation manuelle obsolete

      A mesure que l'ecosysteme adopte le compilateur, les hooks de memoisation manuelle deviendront progressivement inutiles. Comprendre le fonctionnement interne reste cependant essentiel pour diagnostiquer les problemes de performance.

      Arbre de decision final

      Voici la strategie recommandee pour gerer la memoisation dans vos projets React en 2025+.

      decision-tree.tstypescript
      1// ARBRE DE DECISION : memoisation React
      2
      3// 1. Avez-vous active le React Compiler ?
      4// OUI → N'ecrivez aucune memoisation manuelle.
      5// Le compilateur gere tout automatiquement.
      6// Si un composant est lent, profilez et cherchez
      7// un probleme algorithmique, pas un probleme de memo.
      8//
      9// NON → Continuez vers l'etape 2.
      10
      11// 2. Y a-t-il un probleme de performance mesure ?
      12// NON → N'ajoutez aucune memoisation. Revenez quand
      13// un utilisateur ou un profiling montre un probleme.
      14//
      15// OUI → Continuez vers l'etape 3.
      16
      17// 3. Le probleme vient-il de re-renders inutiles ?
      18// (Verifier avec le React DevTools Profiler)
      19//
      20// NON → Le probleme est ailleurs (reseau, algorithme, bundle size).
      21// La memoisation ne resoudra rien.
      22//
      23// OUI → Continuez vers l'etape 4.
      24
      25// 4. Quel type de re-render inutile ?
      26//
      27// a) Un composant enfant re-render car ses props n'ont pas change
      28// → React.memo sur l'enfant
      29// → useMemo/useCallback dans le parent pour stabiliser les props
      30//
      31// b) Un calcul couteux est reexecute inutilement
      32// → useMemo sur le calcul
      33//
      34// c) Une fonction recreee cause un re-render d'un enfant memoise
      35// → useCallback sur la fonction
      36
      37// 5. Re-profilez apres la memoisation.
      38// Si pas d'amelioration mesurable → retirez la memoisation.
      39
      40// RESUME EN UNE PHRASE :
      41// "Activez le compilateur. S'il n'est pas disponible,
      42// mesurez d'abord, memoisez ensuite, re-mesurez pour confirmer."

      Recapitulatif du guide

      React.memo

      HOC qui skip le re-render d'un composant si ses props n'ont pas change (shallow compare). Utile pour les composants couteux avec des props stables.

      useMemo

      Hook qui cache une valeur calculee. Utile pour les calculs couteux (> 1ms) et pour stabiliser les references d'objets passes a des enfants memoises.

      useCallback

      Hook qui cache une fonction. Equivalent a useMemo(() => fn, deps). Utile quand la fonction est passee a un enfant wrape par React.memo.

      Avec le React Compiler, ces trois outils deviennent automatiques. Votre travail se concentre sur l'architecture, la lisibilite et les optimisations algorithmiques.

      Felicitations !

      Vous avez termine ce guide.