Chapitre 1 : useCallback et useMemo
Le problème : re-rendus inutiles
À chaque rendu d'un composant, tout son code est ré-exécuté :
function ProductList({ products }) {
// Recalculé à CHAQUE rendu, même si products n'a pas changé
const sortedProducts = products.sort((a, b) => a.price - b.price);
// Nouvelle fonction créée à CHAQUE rendu
const handleClick = (id) => {
console.log('Clicked:', id);
};
return (
<ul>
{sortedProducts.map(p => (
<ProductItem key={p.id} product={p} onClick={handleClick} />
))}
</ul>
);
}
Si products contient 1000 éléments, le tri est fait à chaque rendu, même si rien n'a changé.
useMemo : mémoriser une valeur
useMemo mémorise le résultat d'un calcul et ne le recalcule que si les dépendances changent.
Syntaxe
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
Exemple : calcul coûteux
import { useMemo } from 'react';
function ProductList({ products, sortBy }) {
// Recalculé SEULEMENT si products ou sortBy change
const sortedProducts = useMemo(() => {
console.log('Tri en cours...');
return [...products].sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
}, [products, sortBy]);
return (
<ul>
{sortedProducts.map(p => (
<li key={p.id}>{p.name} - {p.price}€</li>
))}
</ul>
);
}
Exemple : filtrage
function UserList({ users, searchTerm }) {
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
useCallback : mémoriser une fonction
useCallback mémorise une fonction et ne la recrée que si les dépendances changent.
Syntaxe
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Pourquoi c'est utile ?
En JavaScript, chaque fois qu'une fonction est définie, c'est une nouvelle référence :
const fn1 = () => console.log('hello');
const fn2 = () => console.log('hello');
fn1 === fn2; // false ! Ce sont deux fonctions différentes
Cela pose problème avec React.memo et les dépendances de useEffect.
Exemple avec React.memo
// Composant enfant mémoïsé
const ProductItem = React.memo(function ProductItem({ product, onSelect }) {
console.log('ProductItem rendu:', product.name);
return (
<li onClick={() => onSelect(product.id)}>
{product.name}
</li>
);
});
// Composant parent
function ProductList({ products }) {
const [selected, setSelected] = useState(null);
// SANS useCallback : nouvelle fonction à chaque rendu
// -> ProductItem est re-rendu même si product n'a pas changé
const handleSelect = (id) => {
setSelected(id);
};
// AVEC useCallback : même fonction tant que les dépendances ne changent pas
const handleSelectMemo = useCallback((id) => {
setSelected(id);
}, []); // [] car setSelected est stable
return (
<ul>
{products.map(p => (
<ProductItem
key={p.id}
product={p}
onSelect={handleSelectMemo}
/>
))}
</ul>
);
}
Exemple avec dépendances
function SearchForm({ onSearch, category }) {
const [query, setQuery] = useState('');
// La fonction dépend de category
const handleSubmit = useCallback(() => {
onSearch(query, category);
}, [onSearch, query, category]);
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button type="submit">Rechercher</button>
</form>
);
}
React.memo : mémoriser un composant
React.memo empêche un composant de se re-rendre si ses props n'ont pas changé.
const MyComponent = React.memo(function MyComponent({ name, onClick }) {
console.log('Rendu de MyComponent');
return <button onClick={onClick}>{name}</button>;
});
Important : React.memo compare les props par référence. Si une fonction est recréée à chaque rendu, le composant sera quand même re-rendu.
Quand utiliser useMemo ?
OUI :
- Calculs coûteux (tri, filtrage de grandes listes)
- Création d'objets passés en props à des composants mémoïsés
- Calculs dont le résultat est utilisé dans plusieurs endroits
NON :
- Calculs simples (addition, concaténation)
- Composants qui se re-rendent de toute façon
- Par défaut "au cas où"
Quand utiliser useCallback ?
OUI :
- Fonctions passées à des composants enfants mémoïsés (React.memo)
- Fonctions dans les dépendances de useEffect
- Gestionnaires d'événements coûteux
NON :
- Fonctions utilisées uniquement localement
- Composants enfants non mémoïsés
- Par défaut "pour optimiser"
Comparaison
| Hook | Mémorise | Recalcule si |
|---|---|---|
| useMemo | Une valeur | Les dépendances changent |
| useCallback | Une fonction | Les dépendances changent |
| React.memo | Un composant | Les props changent |
Piège : dépendances incorrectes
// PROBLÈME : items manque dans les dépendances
const getTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, []); // items devrait être dans le tableau !
// CORRECT
const getTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
ESLint avec le plugin eslint-plugin-react-hooks vous avertira des dépendances manquantes.
Exercices
Exercice 1 : Liste filtrée optimisée
Créer une liste de produits avec un champ de recherche. Optimiser pour ne pas refiltrer si la recherche n'a pas changé.
Solution :
function OptimizedProductList({ products }) {
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState('name');
const filteredAndSorted = useMemo(() => {
console.log('Filtrage et tri...');
return products
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price') return a.price - b.price;
return 0;
});
}, [products, search, sortBy]);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Rechercher..."
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Par nom</option>
<option value="price">Par prix</option>
</select>
<ul>
{filteredAndSorted.map(p => (
<li key={p.id}>{p.name} - {p.price}€</li>
))}
</ul>
</div>
);
}
Exercice 2 : Callback stable
Créer un composant parent avec plusieurs boutons enfants. Optimiser pour que les boutons ne se re-rendent pas inutilement.
Solution :
const Button = React.memo(function Button({ id, onClick, children }) {
console.log('Button rendu:', id);
return <button onClick={() => onClick(id)}>{children}</button>;
});
function ButtonGroup() {
const [clicked, setClicked] = useState(null);
const handleClick = useCallback((id) => {
setClicked(id);
console.log('Bouton cliqué:', id);
}, []);
return (
<div>
<p>Dernier clic : {clicked}</p>
<Button id="a" onClick={handleClick}>Bouton A</Button>
<Button id="b" onClick={handleClick}>Bouton B</Button>
<Button id="c" onClick={handleClick}>Bouton C</Button>
</div>
);
}
Points clés à retenir
- useMemo mémorise une valeur calculée
- useCallback mémorise une fonction
- React.memo mémorise un composant
- Ne pas optimiser prématurément - mesurer d'abord
- Dépendances complètes - toujours lister toutes les valeurs utilisées