Retour au blog

Les hooks que tu devrais utiliser plus

useRef, useMemo et useCallback ne sont pas des hooks d'optimisation. Ce sont des hooks de correction qui améliorent aussi la performance.

23 janvier 202612 min de lecture
ReactHooksPerformanceuseRefuseMemouseCallback
Les hooks que tu devrais utiliser plus
Le Trio Mémoire - useRef, useMemo, useCallback
Le Trio Mémoire - useRef, useMemo, useCallback

Tu connais useState et useEffect. Ce sont tes outils du quotidien.

Mais il y a un deuxième tier de hooks que la plupart des devs traitent comme du "truc avancé" ou des "astuces d'optimisation." Ils les évitent jusqu'à ce qu'ils n'aient plus le choix.

C'est à l'envers.

useRef, useMemo, et useCallback ne sont pas des hooks d'optimisation. Ce sont des hooks de correction qui améliorent aussi la performance. Et une fois que tu les comprends, tu te demanderas comment tu as pu coder en React sans eux.

Corrigeons ça.


useRef : la trappe de secours que personne n'utilise assez

La plupart des devs pensent que useRef sert à "accéder aux éléments DOM." C'est vrai, mais ce n'est que la moitié de l'histoire.

useRef est un conteneur mutable qui survit aux re-renders sans les déclencher.

Relis ça. C'est la clé de tout.

Le cas DOM (celui que tu connais)

function TextInput() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>Focus</button>
    </>
  );
}

Tu attaches une ref à un élément DOM, et inputRef.current te donne un accès direct. Classique.

Le cas dont personne ne parle

C'est là que ça devient intéressant. Tu as besoin de stocker une valeur qui :

  • Persiste à travers les renders
  • Ne déclenche pas de re-render quand elle change
  • Peut être lue de façon synchrone (contrairement au state)

C'est useRef.

function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
  }

  return (
    <>
      <p>{time}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

Pourquoi ne pas stocker intervalId dans le state ? Parce que le modifier déclencherait un re-render. Et tu ne veux pas re-render juste parce que tu as stocké un ID de timer.

Le pattern "valeur précédente"

Tu as déjà eu besoin de comparer les props actuelles avec les précédentes ?

function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef(props);

  useEffect(() => {
    const changes = {};
    Object.keys(props).forEach(key => {
      if (previousProps.current[key] !== props[key]) {
        changes[key] = { from: previousProps.current[key], to: props[key] };
      }
    });
    
    if (Object.keys(changes).length > 0) {
      console.log('[why-did-you-update]', name, changes);
    }
    
    previousProps.current = props;
  });
}

Ce hook te dit exactement quelles props ont causé un re-render. Indispensable pour le debug.

Le modèle mental

Pense à useRef comme une boîte posée à côté de ton composant.

La boîte se fiche des renders. Elle garde juste ce que tu mets dedans.


useMemo : arrête de recalculer ce qui n'a pas changé

Voici un composant qui filtre une grosse liste :

function UserList({ users, filter }) {
  const filteredUsers = users.filter(user => 
    user.name.toLowerCase().includes(filter.toLowerCase())
  );

  return (
    <ul>
      {filteredUsers.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

À chaque render, tu filtres toute la liste. Même si users et filter n'ont pas changé.

Avec 10 users ? On s'en fiche. Avec 10 000 users ? Ton UI lag.

Le fix

function UserList({ users, filter }) {
  const filteredUsers = useMemo(() => {
    return users.filter(user => 
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]);

  return (
    <ul>
      {filteredUsers.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Maintenant le filtrage ne s'exécute que quand users ou filter changent vraiment. Tout autre re-render (changement de state parent, mise à jour du context, etc.) skip le travail coûteux.

La vraie raison pour laquelle useMemo existe

La performance c'est bien. Mais il y a une raison plus profonde : la stabilité référentielle.

function Parent() {
  const [count, setCount] = useState(0);
  
  // Nouvel objet à chaque render
  const config = { theme: 'dark', size: 'large' };
  
  return <Child config={config} />;
}

const Child = memo(({ config }) => {
  console.log('Child rendered');
  return <div>{config.theme}</div>;
});

Child est wrappé dans memo, mais il re-render quand même à chaque fois que Parent render. Pourquoi ? Parce que config est un nouvel objet à chaque fois. Mêmes valeurs, référence différente.

function Parent() {
  const [count, setCount] = useState(0);
  
  // Même référence d'objet sauf si les valeurs changent
  const config = useMemo(() => ({ theme: 'dark', size: 'large' }), []);
  
  return <Child config={config} />;
}

Maintenant Child ne re-render que quand la config change vraiment.

Quand NE PAS utiliser useMemo

Ne wrap pas tout dans useMemo. Ça a un coût. Utilise-le quand :

  1. Le calcul est vraiment coûteux (mesure d'abord !)
  2. Tu passes des objets/arrays à des enfants memoized
  3. La valeur est utilisée comme dépendance dans d'autres hooks
// ❌ Overkill
const doubled = useMemo(() => count * 2, [count]);

// ✅ Calcule-le directement
const doubled = count * 2;

Le modèle mental

Pense à useMemo comme un cache avec invalidation automatique.


useCallback : useMemo pour les fonctions

Voilà le truc : useCallback est littéralement juste useMemo pour les fonctions.

// Ces deux sont équivalents
const handleClick = useCallback(() => {
  console.log('clicked');
}, []);

const handleClick = useMemo(() => {
  return () => console.log('clicked');
}, []);

Alors pourquoi useCallback existe ? La commodité. Les fonctions sont tellement souvent passées en props qu'elles ont eu droit à leur propre hook.

Le problème qu'il résout

function SearchPage() {
  const [query, setQuery] = useState('');
  
  function handleSearch(term) {
    // fetch results...
  }
  
  return <SearchInput onSearch={handleSearch} />;
}

const SearchInput = memo(({ onSearch }) => {
  console.log('SearchInput rendered');
  return <input onChange={e => onSearch(e.target.value)} />;
});

SearchInput est memoized, mais il re-render à chaque fois que SearchPage render. Parce que handleSearch est une nouvelle fonction à chaque render.

function SearchPage() {
  const [query, setQuery] = useState('');
  
  const handleSearch = useCallback((term) => {
    // fetch results...
  }, []);
  
  return <SearchInput onSearch={handleSearch} />;
}

Maintenant handleSearch garde la même référence. SearchInput reste memoized.

Le piège des dépendances useEffect

C'est là que useCallback brille vraiment :

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  
  function createConnection() {
    return connectToRoom(roomId);
  }
  
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createConnection]); // 🔴 Nouvelle fonction à chaque render = boucle infinie !
}

L'effect dépend de createConnection, qui est nouvelle à chaque render. Donc l'effect s'exécute à chaque render. Boucle infinie de connexion/déconnexion.

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  
  const createConnection = useCallback(() => {
    return connectToRoom(roomId);
  }, [roomId]);
  
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createConnection]); // ✅ Change seulement quand roomId change
}

Ou encore mieux, déplace la fonction dans l'effect :

useEffect(() => {
  function createConnection() {
    return connectToRoom(roomId);
  }
  
  const connection = createConnection();
  connection.connect();
  return () => connection.disconnect();
}, [roomId]); // ✅ Propre et simple

L'astuce du state updater

Tu dois mettre à jour le state dans un callback mais tu ne veux pas que le callback dépende du state ?

// ❌ handleAdd change à chaque fois que todos change
const handleAdd = useCallback((text) => {
  setTodos([...todos, { id: Date.now(), text }]);
}, [todos]);

// ✅ handleAdd ne change jamais
const handleAdd = useCallback((text) => {
  setTodos(prev => [...prev, { id: Date.now(), text }]);
}, []);

Le pattern de mise à jour fonctionnelle te permet de retirer todos des dépendances complètement.

Le modèle mental


La cheat sheet

HookCe qu'il cacheUtilise quand
useRefValeur mutable (pas de re-render)Accès DOM, timers, valeurs précédentes
useMemoValeur calculéeCalculs coûteux, références stables
useCallbackRéférence de fonctionCallbacks passés aux enfants memoized, dépendances d'effects

Le trio en action

Voici les trois qui travaillent ensemble :

function ProductList({ products, onSelect }) {
  const searchRef = useRef(null);
  const [filter, setFilter] = useState('');
  
  // Mémorise le filtrage coûteux
  const filteredProducts = useMemo(() => {
    return products.filter(p => 
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);
  
  // Callback stable pour les composants enfants
  const handleSelect = useCallback((product) => {
    onSelect(product);
    searchRef.current?.focus();
  }, [onSelect]);
  
  return (
    <div>
      <input 
        ref={searchRef}
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="Rechercher..."
      />
      {filteredProducts.map(product => (
        <ProductCard 
          key={product.id}
          product={product}
          onSelect={handleSelect}
        />
      ))}
    </div>
  );
}

const ProductCard = memo(({ product, onSelect }) => {
  return (
    <div onClick={() => onSelect(product)}>
      {product.name}
    </div>
  );
});
  • useRef pour l'accès DOM sans re-renders
  • useMemo pour le filtrage coûteux
  • useCallback pour la référence de fonction stable
  • memo pour compléter la chaîne d'optimisation

La suite ?

Tu as maintenant les hooks principaux qui gèrent 95% du développement React.

Mais il y a un troisième tier : les hooks qui débloquent des patterns entièrement nouveaux. useReducer pour les machines à état complexes. useLayoutEffect pour les mesures DOM. useImperativeHandle pour exposer des APIs custom.

Dans le prochain article, on explorera ces patterns avancés et quand les utiliser.

Continuer vers la partie 3 : Les hooks qui débloquent de nouveaux patterns →


Des questions ? Trouvé un bug dans mon code ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.