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.

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.
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.
// 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.
'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<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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 <div>Chargement...</div>
if (error) return <div>Erreur : {error}</div>
if (!user) return null
return (
<div className="p-4 border rounded-lg">
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
)
}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.
// Hook générique pour persister l'état dans localStorage
import { useState, useEffect, useCallback } from 'react'
export function useLocalStorage<T>(
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<T>(() => {
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.
'use client'
import { useLocalStorage } from '@/hooks/useLocalStorage'
type Theme = 'light' | 'dark' | 'system'
export function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system')
return (
<select
value={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
className="px-3 py-2 border rounded-lg"
>
<option value="light">Clair</option>
<option value="dark">Sombre</option>
<option value="system">Système</option>
</select>
)
}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.
// 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<CartItem, 'quantity'> }
| { 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<CartItem, 'quantity'>) => {
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.
Prêt à réussir tes entretiens React / Next.js ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
'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 (
<div className="border rounded-lg p-4">
<h3 className="font-medium">{product.name}</h3>
<p className="text-gray-600">{product.price} €</p>
<button
onClick={() => onAddToCart(product.id)}
disabled={!product.inStock}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{product.inStock ? 'Ajouter' : 'Rupture'}
</button>
</div>
)
})
export function ProductList({ products }: ProductListProps) {
const [filter, setFilter] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
const [cart, setCart] = useState<string[]>([])
// 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 (
<div>
<div className="mb-4 flex gap-4">
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Rechercher..."
className="px-3 py-2 border rounded-lg"
/>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}
className="px-3 py-2 border rounded-lg"
>
<option value="name">Nom</option>
<option value="price">Prix</option>
</select>
</div>
<p className="text-sm text-gray-600 mb-4">
{stats.total} produits ({stats.inStock} en stock) -
Prix moyen : {stats.avgPrice.toFixed(2)} €
</p>
<div className="grid grid-cols-3 gap-4">
{filteredAndSortedProducts.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
</div>
)
}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.
// Hook générique de debounce
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(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
}'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<SearchResult[]>([])
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 (
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Rechercher..."
className="w-full px-4 py-2 border rounded-lg"
/>
{loading && (
<div className="absolute right-3 top-2.5">
<div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
)}
{results.length > 0 && (
<ul className="absolute w-full mt-1 bg-white border rounded-lg shadow-lg">
{results.map((result) => (
<li
key={result.id}
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
{result.title}
</li>
))}
</ul>
)}
</div>
)
}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.
'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<VideoPlayerRef, VideoPlayerProps>(
function VideoPlayer({ src, poster }, ref) {
const videoRef = useRef<HTMLVideoElement>(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 (
<div className="relative">
<video
ref={videoRef}
src={src}
poster={poster}
className="w-full rounded-lg"
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
<button
onClick={handlePlayPause}
className="absolute bottom-4 left-4 px-4 py-2 bg-black/50 text-white rounded"
>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
)
}
)'use client'
import { useRef } from 'react'
import { VideoPlayer, VideoPlayerRef } from './VideoPlayer'
export function VideoController() {
const playerRef = useRef<VideoPlayerRef>(null)
const handleSkipForward = () => {
if (playerRef.current) {
const currentTime = playerRef.current.getCurrentTime()
playerRef.current.seek(currentTime + 10)
}
}
return (
<div className="space-y-4">
<VideoPlayer
ref={playerRef}
src="/videos/demo.mp4"
poster="/images/poster.jpg"
/>
<div className="flex gap-2">
<button
onClick={() => playerRef.current?.play()}
className="px-4 py-2 bg-green-600 text-white rounded"
>
Play
</button>
<button
onClick={() => playerRef.current?.pause()}
className="px-4 py-2 bg-red-600 text-white rounded"
>
Pause
</button>
<button
onClick={handleSkipForward}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
+10s
</button>
</div>
</div>
)
}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.
// Hook générique pour les requêtes HTTP
import { useState, useEffect, useCallback, useRef } from 'react'
interface UseFetchState<T> {
data: T | null
loading: boolean
error: Error | null
}
interface UseFetchOptions {
immediate?: boolean // Exécuter immédiatement au montage
onSuccess?: <T>(data: T) => void
onError?: (error: Error) => void
}
export function useFetch<T>(
url: string,
options: UseFetchOptions = {}
) {
const { immediate = true, onSuccess, onError } = options
const [state, setState] = useState<UseFetchState<T>>({
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<AbortController | null>(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.
Prêt à réussir tes entretiens React / Next.js ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

React Compiler en 2026 : mémoïsation automatique et questions d'entretien
Analyse approfondie du React Compiler en 2026 : pipeline de compilation, mémoïsation automatique, règles de React et questions posées en entretien technique.

React 19 : Server Components en production, le guide complet
Maîtrisez les Server Components de React 19 en production. Architecture, patterns, streaming, cache et optimisations pour des applications performantes.

React Server Components en production : patterns et pièges à éviter
React Server Components en production : patterns éprouvés, anti-patterns fréquents et stratégies de débogage pour des applications Next.js 15 robustes.