Retour au blog
oct. 2026·3 min de lectureGénéré par IA
Langue:DeutschEnglishFrançaisÖsterreichischKlingonian Version originale: English

Créer un hook personnalisé de récupération de données en React

#react#javascript#frontend

Toute base de code React finit par accumuler une douzaine de blocs useEffect quasi identiques qui récupèrent des données, suivent l'état de chargement et gèrent les erreurs. Extraire ce schéma dans un seul hook réutilisable, ce n'est pas seulement moins de frappe : bien fait, cela corrige aussi quelques bugs subtils que les useEffect de fetch copiés-collés ont presque toujours.

La version naïve, et ce qui ne va pas

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Cela paraît raisonnable, mais il y a un vrai bug : si userId change rapidement (navigation rapide entre profils), plusieurs fetchs sont en cours en même temps, et rien ne garantit que la dernière réponse à arriver corresponde au userId actuel. Vous pouvez finir par afficher un utilisateur périmé parce qu'une requête plus ancienne et plus lente s'est terminée après une plus récente et plus rapide.

Extraire le hook, avec annulation

function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    let cancelled = false;
    setState({ data: null, loading: true, error: null });

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed: ${res.status}`);
        return res.json();
      })
      .then((data) => {
        if (!cancelled) setState({ data, loading: false, error: null });
      })
      .catch((error) => {
        if (!cancelled) setState({ data: null, loading: false, error });
      });

    return () => {
      cancelled = true;
    };
  }, [url]);

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

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;
  return <h1>{user.name}</h1>;
}

L'indicateur cancelled est la correction clé : quand url change (ou que le composant est démonté), la fonction de nettoyage de l'effet s'exécute et passe cancelled à true pour l'exécution précédente de l'effet. Si ce fetch plus ancien se termine plus tard, son callback .then() vérifie l'indicateur et abandonne au lieu d'écraser un état plus récent avec des données périmées.

Utiliser AbortController au lieu d'un booléen

L'indicateur cancelled empêche React d'agir sur une réponse périmée, mais la requête réseau elle-même continue jusqu'au bout, gaspillant de la bande passante. AbortController annule réellement la requête en cours :

function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed: ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ data, loading: false, error: null }))
      .catch((error) => {
        if (error.name !== 'AbortError') {
          setState({ data: null, loading: false, error });
        }
      });

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

  return state;
}

La vérification error.name !== 'AbortError' est importante : annuler un fetch rejette sa promesse, et sans cette vérification, chaque annulation déclenchée par le nettoyage définirait à tort un état d'erreur sur un composant qui est déjà passé à autre chose.

Ajouter un refetch manuel

Une dépendance url fixe gère le rechargement automatique quand les props changent, mais il faut parfois un bouton « Réessayer » qui recharge la même URL. Il faut alors un moyen de relancer l'effet sans changer url :

function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });
  const [refetchIndex, setRefetchIndex] = useState(0);

  useEffect(() => {
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed: ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ data, loading: false, error: null }))
      .catch((error) => {
        if (error.name !== 'AbortError') {
          setState({ data: null, loading: false, error });
        }
      });

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

  const refetch = () => setRefetchIndex((n) => n + 1);

  return { ...state, refetch };
}

refetchIndex n'affecte pas la requête elle-même : il existe uniquement pour servir de dépendance à laquelle l'effet réagit, ce qui permet aux appelants de forcer une nouvelle exécution sans changer url.

Ce que ce hook ne fait toujours pas

Cette version est une base solide, mais ce n'est volontairement pas une solution complète de récupération de données. Elle ne met pas les réponses en cache entre les composants qui demandent la même URL, ne dédoublonne pas les requêtes simultanées pour la même ressource et ne réessaie pas en cas d'échec transitoire. C'est exactement le vide que comblent des bibliothèques comme React Query et SWR : elles reposent sur les mêmes fondamentaux useEffect + annulation présentés ici, avec en plus un cache partagé et une couche de dédoublonnage des requêtes. Écrire sa propre version vaut la peine une fois, pour comprendre ce qui se passe réellement ; recourir à l'une de ces bibliothèques dès que votre application compte plus de quelques composants dépendant de données est généralement le bon choix.

À retenir

Un hook de récupération de données est en réalité une machine à états (inactif → chargement → succès/erreur) enveloppant un useEffect soigneux sur le nettoyage. La logique d'annulation n'est pas un raffinement facultatif : sans elle, des changements rapides de props produisent silencieusement des conditions de concurrence où une ancienne réponse écrase une nouvelle.

Plus récentL'algorithme des trigrammes : la recherche floue sans magiePlus ancienCSS Container Queries : un guide pratique