Table of Contents

Chapitre 1 : useCallback et useMemo

Le problème : re-rendus inutiles

À chaque rendu d'un composant, tout son code est ré-exécuté :

snippet.javascript
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

snippet.javascript
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);

Exemple : calcul coûteux

snippet.javascript
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

snippet.javascript
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

snippet.javascript
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 :

snippet.javascript
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

snippet.javascript
// 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

snippet.javascript
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é.

snippet.javascript
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 :

NON :

Quand utiliser useCallback ?

OUI :

NON :

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

snippet.javascript
// 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 :

snippet.javascript
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 :

snippet.javascript
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

  1. useMemo mémorise une valeur calculée
  2. useCallback mémorise une fonction
  3. React.memo mémorise un composant
  4. Ne pas optimiser prématurément - mesurer d'abord
  5. Dépendances complètes - toujours lister toutes les valeurs utilisées

← Retour au module | Chapitre suivant : Custom Hooks →