Tutoriel React — Chapitre 21

Et après React ?

Chapitre 21 — Et après React ?

Les hooks React ne sont pas réservés à React : vous pouvez créer vos propres hooks . L’objectif est simple : réutiliser de la logique sans dupliquer du code, tout en gardant vos composants plus lisibles. Dans ce chapitre, on construit deux hooks très utiles : useLocalStorage et useFetch .

Objectif : comprendre (pas apprendre par cœur) Niveau : débutant absolu Pratique : mini-exercice à la fin

Plan du chapitre

1) Pourquoi créer un hook personnalisé ?

Un hook personnalisé sert à extraire une logique répétée. Exemple : gérer une valeur persistée dans localStorage, ou faire un fetch avec loading/erreur.

Un hook personnalisé n’ajoute pas de magie : c’est juste une fonction. La différence : il utilise (ou combine) des hooks React à l’intérieur.

2) Règles d’or des hooks

  • Un hook personnalisé commence par use : useLocalStorage
  • Les hooks doivent être appelés au top-level (pas dans un if / boucle)
  • Un hook peut appeler d’autres hooks
  • Un hook doit rester “prévisible” (pas de comportements cachés)

Pensez à un hook comme une “brique de logique” réutilisable.

3) Structure typique d’un hook

Un hook personnalisé :

  • reçoit des paramètres
  • utilise useState/useEffect/useMemo
  • retourne des valeurs et/ou des fonctions
export function useSomething(param) {
  const [value, setValue] = useState(...);

  useEffect(() => {
    // ...
  }, [param]);

  return { value, setValue };
}

4) Hook 1 : useLocalStorage

Objectif : stocker une valeur dans le state et la persister dans localStorage, pour qu’elle reste même après un refresh.

A) Créer le hook (ex : src/hooks/useLocalStorage.js)

import { useEffect, useState } from "react";

export function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // en cas d'erreur (quota, mode privé, etc.), on ignore
    }
  }, [key, value]);

  return [value, setValue];
}

B) Utilisation dans un composant

import { useLocalStorage } from "./hooks/useLocalStorage.js";

export default function Settings() {
  const [theme, setTheme] = useLocalStorage("theme", "light");

  return (
    <div>
      <p>Thème : {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Basculer
      </button>
    </div>
  );
}

Remarquez l’astuce : useState(() => ...) (fonction) permet de lire le storage une seule fois au montage, pas à chaque rendu.

5) Hook 2 : useFetch (chargement / erreur / données)

Objectif : encapsuler la logique “classique” d’un fetch : loading, error, data.

A) Version simple (débutant)

import { useEffect, useState } from "react";

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;

    let isMounted = true;
    setLoading(true);
    setError(null);

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error("Erreur HTTP : " + res.status);
        return res.json();
      })
      .then((json) => {
        if (isMounted) setData(json);
      })
      .catch((err) => {
        if (isMounted) setError(err.message);
      })
      .finally(() => {
        if (isMounted) setLoading(false);
      });

    return () => {
      isMounted = false;
    };
  }, [url]);

  return { data, loading, error };
}

B) Utilisation

import { useFetch } from "./hooks/useFetch.js";

export default function Users() {
  const { data, loading, error } = useFetch("https://jsonplaceholder.typicode.com/users");

  if (loading) return <p>Chargement...</p>
  if (error) return <p>Erreur : {error}</p>
  if (!data) return null;

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

On isole la logique réseau dans le hook, et le composant reste clair : il affiche selon l’état.

6) Nettoyage (cleanup) et annulation

Pourquoi le cleanup ? Si l’utilisateur change de page pendant un fetch, le composant peut être démonté. Sans protection, on peut essayer de faire un setState sur un composant démonté.

Dans notre version débutant, on utilise isMounted. Dans une version plus moderne, on peut utiliser AbortController (chapitre avancé).

useEffect(() => {
  const controller = new AbortController();

  fetch(url, { signal: controller.signal })
    .then(...)
    .catch((err) => {
      if (err.name === "AbortError") return;
      // sinon vraie erreur
    });

  return () => controller.abort();
}, [url]);

7) Bonnes pratiques

  • Un hook doit avoir un rôle clair (1 responsabilité).
  • Retournez des données/fonctions simples, faciles à comprendre.
  • Évitez les hooks “fourre-tout”.
  • Placez vos hooks dans src/hooks/.
  • Nommez clairement : useAuth, useTheme, useFetch.

8) Résumé (à retenir)

  • Un hook personnalisé factorise une logique et évite la duplication.
  • Il commence par use et peut appeler d’autres hooks.
  • useLocalStorage : state + persistance.
  • useFetch : data/loading/error + cleanup.

9) Exercice pratique

Objectif : créer un hook useCounter et un hook useLocalStorage utilisé dans une page.

A) Hook useCounter

  • State : count
  • Fonctions : inc(), dec(), reset()
  • Retour : { count, inc, dec, reset }

B) Stocker la valeur du compteur en localStorage

Bonus : reliez le compteur à useLocalStorage pour garder la valeur après refresh.

Prochaine étape : Chapitre 22 — Appels API “propres” : séparer services/, gérer les erreurs, et préparer une app pour un backend.