Contactez-nous

Les hooks personnalisés (Custom Hooks)

Apprenez à créer vos propres Hooks personnalisés en React pour encapsuler et réutiliser la logique d'état et d'effets entre différents composants.

Introduction : Factoriser la logique stateful

Au fur et à mesure que vous construisez des applications React, vous remarquerez peut-être que vous répétez souvent la même logique d'état ou les mêmes effets de bord dans différents composants. Par exemple, plusieurs composants pourraient avoir besoin de récupérer des données d'une API de manière similaire, de s'abonner à un événement du navigateur, ou de gérer un état synchronisé avec le `localStorage`.

Copier-coller cette logique d'un composant à l'autre est une mauvaise pratique : cela rend le code difficile à maintenir, à mettre à jour et augmente le risque d'erreurs. Pour résoudre ce problème et promouvoir la réutilisabilité, React introduit le concept de Hooks personnalisés (Custom Hooks).

Un Hook personnalisé est simplement une fonction JavaScript dont le nom commence par use et qui peut appeler d'autres Hooks (comme useState, useEffect, ou même d'autres Hooks personnalisés). Ils permettent d'extraire la logique de composant (état, effets, contexte, etc.) dans des fonctions réutilisables, distinctes de la logique de rendu.

Motivation : Le principe DRY (Don't Repeat Yourself)

Le principal objectif des Hooks personnalisés est d'adhérer au principe DRY ("Ne vous répétez pas"). Plutôt que d'avoir la même configuration de `useState` et `useEffect` pour gérer, disons, la largeur de la fenêtre dans plusieurs composants, vous pouvez encapsuler cette logique dans un seul Hook personnalisé, par exemple useWindowWidth.

Prenons un exemple simple : imaginons deux composants qui ont besoin de gérer un état de compteur simple avec des fonctions d'incrémentation et de décrémentation.

// Composant 1
function CounterA() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  // ... JSX utilisant count, increment, decrement
}

// Composant 2
function CounterB() {
  const [count, setCount] = useState(10); // Valeur initiale différente
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  // ... JSX utilisant count, increment, decrement
}
// La logique useState/increment/decrement est répétée.

Cette répétition peut être évitée en extrayant la logique dans un Hook personnalisé.

Création d'un Hook personnalisé : Règles et conventions

Créer un Hook personnalisé est simple, mais il faut respecter deux règles essentielles (les mêmes que pour les Hooks intégrés) :

  1. Le nom doit commencer par `use` : C'est une convention obligatoire qui permet à React et aux linters (comme `eslint-plugin-react-hooks`) d'identifier qu'il s'agit d'un Hook et d'appliquer les règles des Hooks (ne pas appeler dans des boucles, conditions, etc.). Exemple : useCounter, useFetchData.
  2. Appeler d'autres Hooks uniquement au niveau supérieur : A l'intérieur de votre Hook personnalisé, vous pouvez appeler `useState`, `useEffect`, `useContext`, `useReducer`, `useRef`, `useCallback`, `useMemo`, et même d'autres Hooks personnalisés, mais toujours au niveau supérieur de la fonction, jamais à l'intérieur de conditions, boucles ou fonctions imbriquées.

Un Hook personnalisé est une fonction qui :

  • Prend éventuellement des arguments (par exemple, une valeur initiale, une URL d'API).
  • Utilise un ou plusieurs Hooks React intégrés ou d'autres Hooks personnalisés.
  • Retourne généralement une valeur, un tableau de valeurs, ou un objet contenant l'état et/ou les fonctions nécessaires au composant qui l'utilise.

Important : Chaque appel à un Hook personnalisé crée un état et des effets indépendants. Si deux composants utilisent le même Hook personnalisé, ils ne partagent pas l'état ; chacun obtient sa propre copie de l'état géré par le Hook.

Exemple 1 : `useCounter`

Extrayons la logique de compteur de l'exemple précédent dans un Hook personnalisé :

Définition du Hook personnalisé (`hooks/useCounter.js`) :
import { useState, useCallback } from 'react';

// Le nom commence par 'use'
function useCounter(initialValue = 0) {
  // Appelle useState à l'intérieur
  const [count, setCount] = useState(initialValue);

  // Fonctions mémorisées avec useCallback
  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);
  const reset = useCallback(() => setCount(initialValue), [initialValue]);

  // Retourne l'état et les fonctions pour le composant
  return { count, increment, decrement, reset };
}

export default useCounter;
Utilisation dans les composants :
import React from 'react';
import useCounter from './hooks/useCounter';

function CounterA() {
  // Utilise le hook personnalisé
  const { count, increment, decrement } = useCounter(0);

  return (
    

Compteur A: {count}

); } function CounterB() { // Utilise le même hook, mais avec une valeur initiale différente // Obtient son propre état indépendant const { count, increment, decrement, reset } = useCounter(10); return (

Compteur B: {count}

); } // Le code des composants est maintenant plus simple et la logique est réutilisée.

Exemple 2 : `useFetch` (simplifié)

Un cas d'usage très courant est d'encapsuler la logique de récupération de données, y compris la gestion des états de chargement et d'erreur.

Définition du Hook (`hooks/useFetch.js`) :
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Ignore si l'URL est nulle ou vide
    if (!url) {
      setIsLoading(false);
      return;
    }

    const controller = new AbortController();
    setIsLoading(true);
    setData(null); // Reset data on new fetch
    setError(null); // Reset error on new fetch

    fetch(url, { signal: controller.signal })
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .then(fetchedData => {
        setData(fetchedData);
      })
      .catch(fetchError => {
        if (fetchError.name !== 'AbortError') {
          setError(fetchError.message);
        }
      })
      .finally(() => {
         // Vérifier si le composant n'est pas démonté
         // (peut être amélioré avec une ref isMounted)
        if (!controller.signal.aborted) {
            setIsLoading(false);
        }
      });

    // Nettoyage : annule la requête si l'URL change ou si le composant est démonté
    return () => {
      controller.abort();
    };

  }, [url]); // Se ré-exécute si l'URL change

  // Retourne l'état nécessaire au composant
  return { data, isLoading, error };
}

export default useFetch;
Utilisation dans un composant :
import React from 'react';
import useFetch from './hooks/useFetch';

function UserProfile({ userId }) {
  const apiUrl = `/api/users/${userId}`;
  const { data: user, isLoading, error } = useFetch(apiUrl);

  if (isLoading) return 
Chargement...
; if (error) return
Erreur: {error}
; if (!user) return null; return (

Profil de {user.name}

Email: {user.email}

); }

Ce composant `UserProfile` est maintenant très simple, toute la complexité du fetch (état de chargement, erreur, annulation) est gérée par le Hook `useFetch`.

Exemple 3 : `useLocalStorage`

Gérer un état qui est synchronisé avec le `localStorage` du navigateur.

Définition du Hook (`hooks/useLocalStorage.js`) :
import { useState, useEffect } from 'react';

function getStorageValue(key, defaultValue) {
  // Obtenir la valeur stockée ou la valeur par défaut
  const saved = localStorage.getItem(key);
  const initial = saved !== null ? JSON.parse(saved) : defaultValue;
  return initial;
}

function useLocalStorage(key, defaultValue) {
  const [value, setValue] = useState(() => {
    // Initialisation paresseuse depuis localStorage
    return getStorageValue(key, defaultValue);
  });

  // Met à jour localStorage lorsque la valeur change
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

export default useLocalStorage;
Utilisation :
import React from 'react';
import useLocalStorage from './hooks/useLocalStorage';

function SettingsForm() {
  const [theme, setTheme] = useLocalStorage('appTheme', 'light');
  const [notificationsEnabled, setNotificationsEnabled] = useLocalStorage('notifications', true);

  return (
    
); }

Partager la logique non visuelle

L'essence des Hooks personnalisés est de permettre le partage de logique stateful (qui utilise l'état ou les effets), et non de l'interface utilisateur elle-même. Ils aident à découpler la logique métier ou la gestion d'état complexe de la présentation visuelle.

Cela conduit à des composants UI plus simples, plus focalisés sur le rendu, et à une logique complexe encapsulée, testable et réutilisable à travers l'application. C'est une technique fondamentale pour construire des applications React maintenables et scalables.

Conclusion : Un outil puissant pour la réutilisabilité

Les Hooks personnalisés sont l'une des fonctionnalités les plus puissantes introduites par les Hooks React. Ils offrent un moyen élégant et efficace d'extraire et de réutiliser la logique liée à l'état et aux effets de bord, sans avoir recours à des patterns plus complexes comme les Higher-Order Components (HOC) ou les Render Props qui étaient courants auparavant.

En apprenant à identifier les logiques répétitives et à les encapsuler dans des Hooks personnalisés bien nommés et bien conçus, vous pouvez considérablement améliorer la structure, la lisibilité et la maintenabilité de vos applications React.