ITSkillsCenter
Blog

React hooks : patterns avancés et bonnes pratiques 2026

13 min de lecture

Lecture : 13 minutes · Niveau : intermédiaire-avancé · Mise à jour : avril 2026

Les hooks ont transformé React depuis 2019, mais leur maîtrise demande plus que la connaissance de useState et useEffect. Cet article rassemble les patterns avancés qui distinguent un code React propre d’un code qui marche par accident : custom hooks bien construits, useEffect qui ne fuit pas, gestion d’état complexe avec useReducer, et les pièges qui mordent silencieusement en production.

Voir aussi → React pour PME : guide frontend pro.


Sommaire

  1. Les règles fondamentales des hooks
  2. Custom hooks : factoriser intelligemment
  3. useEffect : quand l’utiliser et quand l’éviter
  4. Dépendances de hooks : exhaustive ou non ?
  5. useReducer pour la logique complexe
  6. useRef au-delà de l’accès DOM
  7. useImperativeHandle pour APIs impératives
  8. Gestion async dans les hooks
  9. Pièges courants et anti-patterns
  10. FAQ

1. Les règles fondamentales des hooks

Deux règles non-négociables :

  1. Appeler les hooks au top-level uniquement : pas dans des conditions, boucles, fonctions imbriquées.
  2. Appeler les hooks depuis des composants ou autres hooks uniquement : pas dans des fonctions classiques.
// Mauvais : hook conditionnel
function Composant({ id }: { id?: number }) {
  if (id) {
    const data = useQuery(["data", id], ...);  // erreur
  }
  return null;
}

// Bon : hook toujours appelé, condition à l'intérieur
function Composant({ id }: { id?: number }) {
  const data = useQuery({
    queryKey: ["data", id],
    queryFn: ...,
    enabled: id !== undefined,
  });
  return null;
}

React identifie les hooks par leur ordre d’appel à chaque rendu. Un appel conditionnel casse cet ordre et provoque des bugs subtils. Le linter eslint-plugin-react-hooks détecte ces erreurs — installer impérativement.


2. Custom hooks : factoriser intelligemment

Un custom hook est une fonction dont le nom commence par use et qui peut appeler d’autres hooks. C’est l’outil principal pour factoriser de la logique réutilisable.

Exemple : useDebounce

function useDebounce<T>(value: T, delayMs: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(t);
  }, [value, delayMs]);

  return debounced;
}

// Usage
function SearchBar() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      search(debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

Exemple : useLocalStorage

function useLocalStorage<T>(cle: string, defaut: T): [T, (val: T) => void] {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(cle);
    return stored ? JSON.parse(stored) : defaut;
  });

  function update(val: T) {
    setValue(val);
    localStorage.setItem(cle, JSON.stringify(val));
  }

  return [value, update];
}

Exemple : useOutsideClick

function useOutsideClick(ref: RefObject<HTMLElement>, callback: () => void) {
  useEffect(() => {
    function handler(e: MouseEvent) {
      if (ref.current && !ref.current.contains(e.target as Node)) {
        callback();
      }
    }
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [ref, callback]);
}

Anti-pattern : extraire trop tôt

Ne pas créer un custom hook pour 3 lignes utilisées une seule fois. Attendre la duplication avérée (règle de 3) ou un cas où le hook clarifie nettement l’intention.


3. useEffect : quand l’utiliser et quand l’éviter

useEffect est le hook le plus mal utilisé. Sa raison d’être : synchroniser le composant avec un système externe (DOM, abonnement, requête réseau, timer).

Bons usages

  • Souscrire à un événement DOM ou WebSocket
  • Lancer une requête réseau (mais préférer TanStack Query)
  • Manipuler le DOM directement
  • Démarrer/arrêter un timer
  • Mettre à jour le document.title

Mauvais usages courants

// Mauvais : effet pour calculer un état dérivé
const [filtered, setFiltered] = useState([]);
useEffect(() => {
  setFiltered(items.filter(i => i.actif));
}, [items]);

// Bon : calcul direct dans le rendu
const filtered = items.filter(i => i.actif);
// ou si coûteux
const filtered = useMemo(() => items.filter(i => i.actif), [items]);
// Mauvais : effet pour appeler le parent
useEffect(() => {
  onValueChange(value);
}, [value, onValueChange]);

// Bon : appeler dans le handler qui modifie value
function handleChange(newValue: string) {
  setValue(newValue);
  onValueChange(newValue);
}

Cleanup obligatoire

Tout effet qui démarre quelque chose doit le nettoyer :

useEffect(() => {
  const id = setInterval(() => tick(), 1000);
  return () => clearInterval(id);
}, []);

useEffect(() => {
  const ctrl = new AbortController();
  fetch(url, { signal: ctrl.signal }).then(...);
  return () => ctrl.abort();
}, [url]);

Sans cleanup : fuites de mémoire, callbacks orphelins, requêtes annulées trop tard.


4. Dépendances de hooks : exhaustive ou non ?

Le linter react-hooks/exhaustive-deps impose de lister toutes les valeurs externes utilisées dans un effet. Suivre cette règle évite des bugs subtils.

// Le linter signale : id devrait être dans deps
useEffect(() => {
  fetch(`/api/users/${id}`);
}, []);

// Correct
useEffect(() => {
  fetch(`/api/users/${id}`);
}, [id]);

Cas où on est tenté de désobéir

// Veut s'exécuter une seule fois au montage
useEffect(() => {
  trackPageView(currentUrl);
}, []);  // currentUrl manquant

Solutions propres : extraire la valeur dans un useRef (lecture sans dépendance), ou utiliser un useEffect avec une condition explicite.

const tracked = useRef(false);
useEffect(() => {
  if (!tracked.current) {
    trackPageView(currentUrl);
    tracked.current = true;
  }
}, [currentUrl]);

Stabiliser les fonctions et objets

Si une dépendance est une fonction ou un objet recréé à chaque rendu, l’effet se relance à chaque rendu :

// Problème : config est nouveau à chaque rendu
const config = { timeout: 5000 };
useEffect(() => {
  init(config);
}, [config]);  // se relance à chaque rendu

// Solution : useMemo / useCallback / extraction
const config = useMemo(() => ({ timeout: 5000 }), []);

Avec React Compiler (React 19+), beaucoup de ces stabilisations deviennent automatiques.


5. useReducer pour la logique complexe

Quand l’état a plusieurs sous-valeurs liées, useReducer clarifie souvent par rapport à plusieurs useState.

type State = {
  status: "idle" | "loading" | "success" | "error";
  data: User[] | null;
  error: Error | null;
};

type Action =
  | { type: "fetch_start" }
  | { type: "fetch_success"; data: User[] }
  | { type: "fetch_error"; error: Error }
  | { type: "reset" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "fetch_start":
      return { ...state, status: "loading", error: null };
    case "fetch_success":
      return { status: "success", data: action.data, error: null };
    case "fetch_error":
      return { ...state, status: "error", error: action.error };
    case "reset":
      return { status: "idle", data: null, error: null };
  }
}

function UsersList() {
  const [state, dispatch] = useReducer(reducer, {
    status: "idle",
    data: null,
    error: null,
  });

  useEffect(() => {
    dispatch({ type: "fetch_start" });
    fetchUsers()
      .then(data => dispatch({ type: "fetch_success", data }))
      .catch(error => dispatch({ type: "fetch_error", error }));
  }, []);

  if (state.status === "loading") return <Spinner />;
  if (state.status === "error") return <Error error={state.error} />;
  if (state.status === "success") return <List items={state.data} />;
  return null;
}

Avantages : transitions d’état explicites, impossible d’avoir un état impossible (ex: loading: true et data: [...] simultanément), logique testable indépendamment du composant.


6. useRef au-delà de l’accès DOM

useRef retourne un objet mutable qui survit aux re-renders. Usage classique : référence DOM. Mais ses autres usages sont précieux.

Stocker une valeur sans déclencher de rerender

function ChatRoom() {
  const messageCount = useRef(0);

  function onMessage() {
    messageCount.current += 1;
    console.log(`Total: ${messageCount.current}`);
  }

  return <div onClick={onMessage}>...</div>;
}

useState re-rendrait à chaque incrément. useRef non.

Stocker la dernière version d’une fonction

Pattern utile pour les callbacks dans des effets :

function useEvent<T extends (...args: any[]) => any>(handler: T): T {
  const ref = useRef(handler);
  useLayoutEffect(() => {
    ref.current = handler;
  });
  return useCallback((...args: any[]) => ref.current(...args), []) as T;
}

useEvent (proposition officielle React) garantit que la callback peut accéder aux dernières props/state sans relancer les effets qui en dépendent.

Référence vers une instance d’objet

const wsRef = useRef<WebSocket | null>(null);

useEffect(() => {
  wsRef.current = new WebSocket(url);
  return () => wsRef.current?.close();
}, [url]);

function send(msg: string) {
  wsRef.current?.send(msg);
}

7. useImperativeHandle pour APIs impératives

Cas rare mais utile : un composant enfant doit exposer des méthodes appelables depuis le parent.

import { forwardRef, useImperativeHandle, useRef } from "react";

interface MyInputHandle {
  focus(): void;
  reset(): void;
}

const MyInput = forwardRef<MyInputHandle, {}>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    reset: () => {
      if (inputRef.current) inputRef.current.value = "";
    },
  }));

  return <input ref={inputRef} {...props} />;
});

// Usage parent
function Parent() {
  const ref = useRef<MyInputHandle>(null);
  return (
    <>
      <MyInput ref={ref} />
      <button onClick={() => ref.current?.focus()}>Focus</button>
    </>
  );
}

Outil puissant mais à utiliser avec parcimonie : casse l’idiome déclaratif de React. Préférer des props pour piloter le comportement quand possible.


8. Gestion async dans les hooks

// Mauvais : async direct dans useEffect
useEffect(async () => {           // erreur : useEffect attend une cleanup, pas une promise
  const data = await fetch(url);
}, []);

// Bon : fonction async à l'intérieur
useEffect(() => {
  async function load() {
    const data = await fetch(url);
    setData(data);
  }
  load();
}, [url]);

// Encore mieux : avec annulation
useEffect(() => {
  const ctrl = new AbortController();
  async function load() {
    try {
      const r = await fetch(url, { signal: ctrl.signal });
      setData(await r.json());
    } catch (err) {
      if (err.name !== "AbortError") setError(err);
    }
  }
  load();
  return () => ctrl.abort();
}, [url]);

Race conditions

Si l’utilisateur change de paramètres rapidement, la deuxième requête peut résoudre avant la première et écraser les bonnes données. Solutions : AbortController, ou TanStack Query qui gère ce cas automatiquement.

// Avec un flag de désactivation
useEffect(() => {
  let active = true;
  fetch(url).then(r => r.json()).then(data => {
    if (active) setData(data);
  });
  return () => { active = false; };
}, [url]);

9. Pièges courants et anti-patterns

Mutation d’état

// Mauvais
const [items, setItems] = useState([1, 2, 3]);
items.push(4);
setItems(items);  // React ne voit pas le changement (même référence)

// Bon
setItems([...items, 4]);
// ou
setItems(prev => [...prev, 4]);

Lire l’état après setState

// Mauvais : count est l'ancienne valeur ici
function increment() {
  setCount(count + 1);
  console.log(count);  // affiche la valeur AVANT le setCount
}

// Bon : utiliser le callback de setState
function increment() {
  setCount(prev => {
    const next = prev + 1;
    console.log(next);
    return next;
  });
}

useState avec valeur calculée coûteuse

// Mauvais : calc() s'exécute à chaque rendu
const [data, setData] = useState(calc());

// Bon : initialisation paresseuse
const [data, setData] = useState(() => calc());

Trop de useEffect imbriqués

Si un composant a plus de 3-4 useEffect, c’est souvent un signal qu’il fait trop de choses. Décomposer en sous-composants ou en custom hooks.

Voir aussi → React state management 2026 pour quand sortir l’état du composant.


10. FAQ

useCallback partout est-il une bonne pratique ?

Non. useCallback mémorise une fonction, ce qui n’a de sens que si elle est passée à un composant mémorisé (React.memo) ou utilisée comme dépendance d’un autre hook. Avec React Compiler, la plupart de ces optimisations sont automatiques. Avant React 19, n’utiliser useCallback que quand mesuré comme nécessaire.

Faut-il toujours useMemo pour les calculs coûteux ?

Si le calcul est vraiment coûteux (>quelques ms) et se reproduit souvent : oui. Pour des calculs simples, useMemo peut coûter plus que ce qu’il économise. Profiler avant d’optimiser.

Comment partager un hook entre composants frères ?

Un hook custom partagé donne le même comportement aux deux composants mais des états séparés. Si l’état doit être partagé, remonter au parent commun (lift state up) ou utiliser un store global (Zustand, Context).

Quand utiliser useReducer vs useState ?

useState pour des valeurs indépendantes simples. useReducer quand plusieurs valeurs sont liées et que les transitions ont une logique non-triviale. Si vous écrivez plusieurs setters dans la même fonction de manière coordonnée, useReducer clarifie souvent.

Hooks dans une fonction utilitaire, ça marche ?

Non. Les hooks ne peuvent être appelés que depuis un composant React ou un autre hook custom. Une fonction utilitaire classique qui appelle un hook plante. Préfixer la fonction par use la transforme en hook custom et autorise l’appel.

useLayoutEffect vs useEffect ?

useLayoutEffect s’exécute synchrone après le DOM update mais avant le paint navigateur. Utiliser quand on doit mesurer ou modifier le DOM avant que l’utilisateur voit le résultat (pour éviter le flicker). Sinon useEffect.

React Compiler change-t-il vraiment la donne ?

Oui significativement. Avec React Compiler, beaucoup d’optimisations manuelles (useMemo, useCallback, React.memo) deviennent inutiles. Le code est plus simple à écrire et l’optimisation est automatique. Activer dès que possible sur React 19+.


Articles liés (cluster React)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité