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
- Les règles fondamentales des hooks
- Custom hooks : factoriser intelligemment
useEffect: quand l’utiliser et quand l’éviter- Dépendances de hooks : exhaustive ou non ?
useReducerpour la logique complexeuseRefau-delà de l’accès DOMuseImperativeHandlepour APIs impératives- Gestion async dans les hooks
- Pièges courants et anti-patterns
- FAQ
1. Les règles fondamentales des hooks
Deux règles non-négociables :
- Appeler les hooks au top-level uniquement : pas dans des conditions, boucles, fonctions imbriquées.
- 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)
- 👉 React pour PME : guide frontend pro (pillar)
- 👉 React state management en 2026
- 👉 React performance : optimisation pratique
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.