# React Hooks avancés : Patterns et optimisations > Maîtrisez les Hooks React avancés avec des patterns éprouvés. Custom hooks, useEffect optimisé, useMemo, useCallback et techniques de performance. - Published: 2026-02-16 - Updated: 2026-03-31 - Author: SharpSkill - Tags: react hooks, useEffect, custom hooks, performance, react patterns - Reading time: 12 min --- Les Hooks ont révolutionné React en permettant d'utiliser l'état et les effets dans les composants fonctionnels. Au-delà des usages basiques de `useState` et `useEffect`, des patterns avancés permettent de créer du code réutilisable, performant et maintenable. > **Prérequis** > > Ce guide suppose une connaissance des Hooks de base (useState, useEffect, useContext). Les exemples utilisent React 18+ et TypeScript pour un typage robuste. ## Maîtriser useEffect : éviter les pièges courants Le hook `useEffect` est souvent mal compris et mal utilisé. Une compréhension approfondie de son fonctionnement permet d'éviter les bugs subtils et les problèmes de performance. Le principe fondamental : `useEffect` synchronise un composant avec un système externe. Si l'effet ne communique pas avec l'extérieur (API, DOM, timer), il est probablement inutile. ```tsx // hooks/useDocumentTitle.ts // Custom hook pour synchroniser le titre du document import { useEffect } from 'react' export function useDocumentTitle(title: string) { useEffect(() => { // Sauvegarder le titre précédent const previousTitle = document.title // Mettre à jour le titre document.title = title // Cleanup : restaurer le titre précédent au démontage return () => { document.title = previousTitle } }, [title]) // Se déclenche uniquement si title change } ``` L'erreur la plus courante consiste à omettre le tableau de dépendances ou à y inclure des valeurs instables. Chaque valeur utilisée dans l'effet doit figurer dans les dépendances. ```tsx // components/UserProfile.tsx 'use client' import { useEffect, useState } from 'react' interface User { id: string name: string email: string } interface UserProfileProps { userId: string } export function UserProfile({ userId }: UserProfileProps) { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { // Flag pour éviter les mises à jour après démontage let isMounted = true // Controller pour annuler les requêtes en cours const controller = new AbortController() async function fetchUser() { setLoading(true) setError(null) try { const response = await fetch(`/api/users/${userId}`, { signal: controller.signal }) if (!response.ok) { throw new Error('Utilisateur non trouvé') } const data = await response.json() // Vérifier que le composant est toujours monté if (isMounted) { setUser(data) } } catch (err) { // Ignorer les erreurs d'annulation if (err instanceof Error && err.name !== 'AbortError') { if (isMounted) { setError(err.message) } } } finally { if (isMounted) { setLoading(false) } } } fetchUser() // Cleanup : annuler la requête et marquer comme démonté return () => { isMounted = false controller.abort() } }, [userId]) // Relancer l'effet si userId change if (loading) return
Chargement...
if (error) return
Erreur : {error}
if (!user) return null return (

{user.name}

{user.email}

) } ``` Ce pattern avec `AbortController` et flag `isMounted` évite les fuites de mémoire et les mises à jour d'état sur des composants démontés. ## Créer des custom hooks réutilisables Les custom hooks encapsulent de la logique réutilisable. Un bon custom hook suit le principe de responsabilité unique et expose une API claire. ```tsx // hooks/useLocalStorage.ts // Hook générique pour persister l'état dans localStorage import { useState, useEffect, useCallback } from 'react' export function useLocalStorage( key: string, initialValue: T ): [T, (value: T | ((prev: T) => T)) => void, () => void] { // Initialiser l'état avec la valeur stockée ou la valeur par défaut const [storedValue, setStoredValue] = useState(() => { if (typeof window === 'undefined') { return initialValue } try { const item = window.localStorage.getItem(key) return item ? JSON.parse(item) : initialValue } catch (error) { console.warn(`Erreur lecture localStorage "${key}":`, error) return initialValue } }) // Synchroniser avec localStorage à chaque changement useEffect(() => { if (typeof window === 'undefined') return try { window.localStorage.setItem(key, JSON.stringify(storedValue)) } catch (error) { console.warn(`Erreur écriture localStorage "${key}":`, error) } }, [key, storedValue]) // Fonction pour mettre à jour la valeur const setValue = useCallback((value: T | ((prev: T) => T)) => { setStoredValue((prev) => { const nextValue = value instanceof Function ? value(prev) : value return nextValue }) }, []) // Fonction pour supprimer la valeur const removeValue = useCallback(() => { setStoredValue(initialValue) if (typeof window !== 'undefined') { window.localStorage.removeItem(key) } }, [key, initialValue]) return [storedValue, setValue, removeValue] } ``` Ce hook peut être utilisé dans n'importe quel composant pour persister des données. ```tsx // components/ThemeToggle.tsx 'use client' import { useLocalStorage } from '@/hooks/useLocalStorage' type Theme = 'light' | 'dark' | 'system' export function ThemeToggle() { const [theme, setTheme] = useLocalStorage('theme', 'system') return ( ) } ``` > **Convention de nommage** > > Les custom hooks commencent toujours par "use" (useLocalStorage, useFetch, useDebounce). Cette convention permet à React de vérifier les règles des hooks et aux outils de linting de fonctionner correctement. ## Pattern de composition avec useReducer Pour les états complexes avec plusieurs actions possibles, `useReducer` offre une structure plus prévisible que plusieurs `useState`. ```tsx // hooks/useCart.ts // Hook de gestion de panier avec useReducer import { useReducer, useCallback, useMemo } from 'react' interface CartItem { id: string name: string price: number quantity: number } interface CartState { items: CartItem[] isOpen: boolean } type CartAction = | { type: 'ADD_ITEM'; payload: Omit } | { type: 'REMOVE_ITEM'; payload: string } | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } } | { type: 'CLEAR_CART' } | { type: 'TOGGLE_CART' } function cartReducer(state: CartState, action: CartAction): CartState { switch (action.type) { case 'ADD_ITEM': { const existingItem = state.items.find( (item) => item.id === action.payload.id ) if (existingItem) { // Incrémenter la quantité si l'item existe return { ...state, items: state.items.map((item) => item.id === action.payload.id ? { ...item, quantity: item.quantity + 1 } : item ) } } // Ajouter un nouvel item return { ...state, items: [...state.items, { ...action.payload, quantity: 1 }] } } case 'REMOVE_ITEM': return { ...state, items: state.items.filter((item) => item.id !== action.payload) } case 'UPDATE_QUANTITY': return { ...state, items: state.items.map((item) => item.id === action.payload.id ? { ...item, quantity: action.payload.quantity } : item ).filter((item) => item.quantity > 0) } case 'CLEAR_CART': return { ...state, items: [] } case 'TOGGLE_CART': return { ...state, isOpen: !state.isOpen } default: return state } } const initialState: CartState = { items: [], isOpen: false } export function useCart() { const [state, dispatch] = useReducer(cartReducer, initialState) // Actions mémorisées pour éviter les re-renders const addItem = useCallback( (item: Omit) => { dispatch({ type: 'ADD_ITEM', payload: item }) }, [] ) const removeItem = useCallback((id: string) => { dispatch({ type: 'REMOVE_ITEM', payload: id }) }, []) const updateQuantity = useCallback((id: string, quantity: number) => { dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } }) }, []) const clearCart = useCallback(() => { dispatch({ type: 'CLEAR_CART' }) }, []) const toggleCart = useCallback(() => { dispatch({ type: 'TOGGLE_CART' }) }, []) // Valeurs dérivées mémorisées const total = useMemo( () => state.items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ), [state.items] ) const itemCount = useMemo( () => state.items.reduce((sum, item) => sum + item.quantity, 0), [state.items] ) return { items: state.items, isOpen: state.isOpen, total, itemCount, addItem, removeItem, updateQuantity, clearCart, toggleCart } } ``` Cette approche centralise toute la logique du panier et facilite les tests unitaires. ## Optimisation avec useMemo et useCallback Ces hooks évitent les calculs ou créations de fonctions inutiles. Leur utilisation doit être ciblée : les appliquer partout dégrade les performances au lieu de les améliorer. ```tsx // components/ProductList.tsx 'use client' import { useState, useMemo, useCallback, memo } from 'react' interface Product { id: string name: string price: number category: string inStock: boolean } interface ProductListProps { products: Product[] } // Composant enfant mémorisé const ProductCard = memo(function ProductCard({ product, onAddToCart }: { product: Product onAddToCart: (id: string) => void }) { console.log(`Render ProductCard: ${product.name}`) return (

{product.name}

{product.price} €

) }) export function ProductList({ products }: ProductListProps) { const [filter, setFilter] = useState('') const [sortBy, setSortBy] = useState<'name' | 'price'>('name') const [cart, setCart] = useState([]) // useMemo : mémoriser le résultat d'un calcul coûteux const filteredAndSortedProducts = useMemo(() => { console.log('Calcul filteredAndSortedProducts') let result = products // Filtrer par nom if (filter) { result = result.filter((p) => p.name.toLowerCase().includes(filter.toLowerCase()) ) } // Trier result = [...result].sort((a, b) => { if (sortBy === 'name') { return a.name.localeCompare(b.name) } return a.price - b.price }) return result }, [products, filter, sortBy]) // Recalculer uniquement si ces deps changent // useCallback : mémoriser une fonction const handleAddToCart = useCallback((productId: string) => { setCart((prev) => [...prev, productId]) }, []) // Dépendances vides : la fonction ne change jamais // Statistiques mémorisées const stats = useMemo( () => ({ total: filteredAndSortedProducts.length, inStock: filteredAndSortedProducts.filter((p) => p.inStock).length, avgPrice: filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) / filteredAndSortedProducts.length || 0 }), [filteredAndSortedProducts] ) return (
setFilter(e.target.value)} placeholder="Rechercher..." className="px-3 py-2 border rounded-lg" />

{stats.total} produits ({stats.inStock} en stock) - Prix moyen : {stats.avgPrice.toFixed(2)} €

{filteredAndSortedProducts.map((product) => ( ))}
) } ``` > **Quand utiliser useMemo/useCallback ?** > > Ces hooks ajoutent de la complexité. Les utiliser uniquement pour : (1) des calculs réellement coûteux, (2) des props passées à des composants mémorisés avec `memo()`, (3) des dépendances d'autres hooks. ## Hook de debounce pour les recherches Le debounce évite d'exécuter une action trop fréquemment, typiquement lors de la saisie dans un champ de recherche. ```tsx // hooks/useDebounce.ts // Hook générique de debounce import { useState, useEffect } from 'react' export function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value) useEffect(() => { // Créer un timer qui met à jour la valeur après le délai const timer = setTimeout(() => { setDebouncedValue(value) }, delay) // Cleanup : annuler le timer si value change avant le délai return () => { clearTimeout(timer) } }, [value, delay]) return debouncedValue } ``` ```tsx // components/SearchInput.tsx 'use client' import { useState, useEffect } from 'react' import { useDebounce } from '@/hooks/useDebounce' interface SearchResult { id: string title: string } export function SearchInput() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [loading, setLoading] = useState(false) // Debounce la requête de 300ms const debouncedQuery = useDebounce(query, 300) useEffect(() => { // Ne pas rechercher si la requête est vide if (!debouncedQuery.trim()) { setResults([]) return } async function search() { setLoading(true) try { const response = await fetch( `/api/search?q=${encodeURIComponent(debouncedQuery)}` ) const data = await response.json() setResults(data.results) } catch (error) { console.error('Erreur de recherche:', error) } finally { setLoading(false) } } search() }, [debouncedQuery]) // Déclenché uniquement quand debouncedQuery change return (
setQuery(e.target.value)} placeholder="Rechercher..." className="w-full px-4 py-2 border rounded-lg" /> {loading && (
)} {results.length > 0 && (
    {results.map((result) => (
  • {result.title}
  • ))}
)}
) } ``` Le composant effectue la recherche uniquement 300ms après que l'utilisateur ait arrêté de taper, évitant des dizaines de requêtes inutiles. ## useImperativeHandle pour les refs avancées Ce hook permet d'exposer des méthodes spécifiques d'un composant enfant au parent via une ref. ```tsx // components/VideoPlayer.tsx 'use client' import { useRef, useImperativeHandle, forwardRef, useState, useCallback } from 'react' // Interface des méthodes exposées export interface VideoPlayerRef { play: () => void pause: () => void seek: (time: number) => void getCurrentTime: () => number } interface VideoPlayerProps { src: string poster?: string } export const VideoPlayer = forwardRef( function VideoPlayer({ src, poster }, ref) { const videoRef = useRef(null) const [isPlaying, setIsPlaying] = useState(false) // Exposer uniquement les méthodes souhaitées au parent useImperativeHandle( ref, () => ({ play: () => { videoRef.current?.play() setIsPlaying(true) }, pause: () => { videoRef.current?.pause() setIsPlaying(false) }, seek: (time: number) => { if (videoRef.current) { videoRef.current.currentTime = time } }, getCurrentTime: () => { return videoRef.current?.currentTime ?? 0 } }), [] // Pas de dépendances : les méthodes utilisent toujours la ref actuelle ) const handlePlayPause = useCallback(() => { if (isPlaying) { videoRef.current?.pause() } else { videoRef.current?.play() } setIsPlaying(!isPlaying) }, [isPlaying]) return (
) } ) ``` ```tsx // components/VideoController.tsx 'use client' import { useRef } from 'react' import { VideoPlayer, VideoPlayerRef } from './VideoPlayer' export function VideoController() { const playerRef = useRef(null) const handleSkipForward = () => { if (playerRef.current) { const currentTime = playerRef.current.getCurrentTime() playerRef.current.seek(currentTime + 10) } } return (
) } ``` Ce pattern est particulièrement utile pour les composants médias, formulaires ou toute interface nécessitant un contrôle impératif. ## Hook de gestion du cycle de vie des requêtes Un hook avancé pour gérer les états de chargement, erreurs et données avec typage strict. ```tsx // hooks/useFetch.ts // Hook générique pour les requêtes HTTP import { useState, useEffect, useCallback, useRef } from 'react' interface UseFetchState { data: T | null loading: boolean error: Error | null } interface UseFetchOptions { immediate?: boolean // Exécuter immédiatement au montage onSuccess?: (data: T) => void onError?: (error: Error) => void } export function useFetch( url: string, options: UseFetchOptions = {} ) { const { immediate = true, onSuccess, onError } = options const [state, setState] = useState>({ data: null, loading: immediate, error: null }) // Ref pour éviter les mises à jour après démontage const mountedRef = useRef(true) // Ref pour le controller d'annulation const abortControllerRef = useRef(null) const execute = useCallback(async () => { // Annuler la requête précédente si en cours abortControllerRef.current?.abort() abortControllerRef.current = new AbortController() setState((prev) => ({ ...prev, loading: true, error: null })) try { const response = await fetch(url, { signal: abortControllerRef.current.signal }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const data: T = await response.json() if (mountedRef.current) { setState({ data, loading: false, error: null }) onSuccess?.(data) } return data } catch (error) { // Ignorer les erreurs d'annulation if (error instanceof Error && error.name === 'AbortError') { return null } const err = error instanceof Error ? error : new Error('Erreur inconnue') if (mountedRef.current) { setState({ data: null, loading: false, error: err }) onError?.(err) } return null } }, [url, onSuccess, onError]) const reset = useCallback(() => { setState({ data: null, loading: false, error: null }) }, []) // Exécution au montage si immediate=true useEffect(() => { if (immediate) { execute() } return () => { mountedRef.current = false abortControllerRef.current?.abort() } }, [execute, immediate]) return { ...state, execute, reset, isIdle: !state.loading && !state.data && !state.error } } ``` Ce hook offre une API complète pour gérer n'importe quelle requête avec gestion automatique des annulations et du cycle de vie. ## Conclusion Les patterns avancés des React Hooks permettent de créer du code maintenable et performant. Les points essentiels : - ✅ **useEffect** : toujours inclure le cleanup et gérer les race conditions avec AbortController - ✅ **Custom hooks** : encapsuler la logique réutilisable avec une API claire - ✅ **useReducer** : privilégier pour les états complexes avec plusieurs actions - ✅ **useMemo/useCallback** : utiliser de manière ciblée, uniquement quand nécessaire - ✅ **useDebounce** : limiter les exécutions fréquentes pour les recherches - ✅ **useImperativeHandle** : exposer des méthodes impératives de manière contrôlée La maîtrise de ces patterns distingue les développeurs React intermédiaires des experts. Chaque hook résout un problème spécifique : le choix du bon outil au bon moment est la clé d'une architecture React robuste. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/react-next/react-hooks-avances-patterns-optimisations