Anti-patterns React en 2026 : 6 erreurs courantes
Publié le
Développeur React & Next.js freelance
Ces six anti-patterns React apparaissent dans la majorité des codebases. Ils provoquent des re-renders inutiles, des bugs de synchronisation et de la complexité accidentelle. Le React Compiler (stable depuis octobre 2025) automatise la mémoïsation, mais ne corrige pas ces erreurs de logique.
Chaque section présente le problème, son impact, et la correction avec un exemple avant/après.
Dans cet article :
- Les 6 anti-patterns les plus fréquents, avec code avant/après
- Pourquoi chaque pattern pose problème (re-renders, bugs, complexité)
- Les alternatives modernes (React 19, Server Components)
- Un diagnostic rapide pour détecter ces patterns dans votre codebase
1. Calculer l'état dérivé dans un useEffect
Le problème
C'est l'anti-pattern le plus répandu. Un composant stocke dans le state une valeur qui pourrait être calculée à partir des props ou d'un autre state.
// ❌ Anti-pattern : état dérivé synchronisé dans useEffect
const CartSummary = ({ items }: { items: CartItem[] }) => {
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price * item.quantity, 0))
}, [items])
return <p>Total : {total.toFixed(2)} €</p>
}Ce code provoque deux renders à chaque changement de items :
- Un premier render avec l'ancien
total(stale) - Le
useEffects'exécute, appellesetTotal, et déclenche un second render
Le composant affiche brièvement un montant incorrect entre les deux renders.
La correction
Calculer la valeur directement pendant le render. React ré-exécute la fonction composant à chaque changement de props ou state.
// ✅ Calcul direct pendant le render
const CartSummary = ({ items }: { items: CartItem[] }) => {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
return <p>Total : {total.toFixed(2)} €</p>
}Un seul render, pas de state intermédiaire, pas de désynchronisation.
Si le calcul est coûteux
Pour les opérations coûteuses (calculs sur de longues listes, transformations complexes), useMemo évite de recalculer à chaque render :
// ✅ Calcul mémoïsé pour les opérations coûteuses
const CartSummary = ({ items }: { items: CartItem[] }) => {
const total = useMemo(
() => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
[items],
)
return <p>Total : {total.toFixed(2)} €</p>
}useMemo ne recalcule que si items change. Le React Compiler (opt-in dans Next.js 16 via reactCompiler: true) automatise cette mémoïsation. Avec le Compiler activé, useMemo explicite devient redondant dans la plupart des cas.
Comment détecter cet anti-pattern
Signal d'alerte : un useEffect qui appelle un setter du même composant avec une valeur calculée à partir des deps.
// 🚩 Signal : useEffect + setState avec valeur dérivée
useEffect(() => {
setSomething(computeFrom(dep1, dep2))
}, [dep1, dep2])Deux règles ESLint détectent automatiquement ce pattern :
set-state-in-effectdu plugin officieleslint-plugin-react-hooksv7 (incluse dans le presetrecommended)eslint-plugin-react-you-might-not-need-an-effect(communautaire, détection plus ciblée)
2. Utiliser useState quand useRef suffit
Le problème
useState déclenche un re-render à chaque mise à jour. Quand la valeur stockée n'affecte pas le rendu visuel, ce re-render est inutile.
// ❌ Anti-pattern : useState pour une valeur qui n'affecte pas le rendu
const Stopwatch = () => {
const [startTime, setStartTime] = useState<number | null>(null)
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null)
const [now, setNow] = useState<number | null>(null)
const handleStart = () => {
const start = Date.now()
setStartTime(start)
setNow(start)
// ❌ Stocker l'interval ID dans le state provoque un re-render inutile
setIntervalId(
setInterval(() => {
setNow(Date.now())
}, 10),
)
}
const handleStop = () => {
if (intervalId) clearInterval(intervalId)
}
const elapsed = startTime && now ? (now - startTime) / 1000 : 0
return (
<div>
<p>{elapsed.toFixed(2)}s</p>
<button onClick={handleStart}>Démarrer</button>
<button onClick={handleStop}>Arrêter</button>
</div>
)
}intervalId ne s'affiche nulle part. Le stocker dans useState ajoute du state inutile au composant : React le suit, le compare à chaque render, et le sérialise dans les outils de debug. Ce n'est pas du state UI, c'est un détail d'implémentation.
La correction
useRef persiste la valeur entre les renders sans l'intégrer au state du composant.
// ✅ useRef pour les valeurs qui ne s'affichent pas
const Stopwatch = () => {
const [startTime, setStartTime] = useState<number | null>(null)
const [now, setNow] = useState<number | null>(null)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const handleStart = () => {
const start = Date.now()
setStartTime(start)
setNow(start)
intervalRef.current = setInterval(() => {
setNow(Date.now())
}, 10)
}
const handleStop = () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
const elapsed = startTime && now ? (now - startTime) / 1000 : 0
return (
<div>
<p>{elapsed.toFixed(2)}s</p>
<button onClick={handleStart}>Démarrer</button>
<button onClick={handleStop}>Arrêter</button>
</div>
)
}Règle de décision
| La valeur s'affiche dans le JSX ? | Hook à utiliser |
|---|---|
| Oui | useState |
| Non | useRef |
Cas courants pour useRef :
- Timer IDs (
setTimeout,setInterval) - Références DOM
- Valeurs précédentes (previous props/state)
- Compteurs internes (nombre d'appels API, tentatives)
- Instances de classes externes (IntersectionObserver, WebSocket)
3. Copier les props dans le state
Le problème
Dupliquer une prop dans le state local crée deux sources de vérité. Le state local diverge silencieusement quand la prop change.
// ❌ Anti-pattern : copier une prop dans le state
const UserProfile = ({ user }: { user: User }) => {
const [name, setName] = useState(user.name)
// ❌ Le state initial est capturé une seule fois.
// Si `user.name` change, `name` garde l'ancienne valeur.
return <p>{name}</p>
}useState(user.name) capture la valeur au premier render uniquement. Les mises à jour suivantes de user.name sont ignorées.
La correction
Utiliser la prop directement :
// ✅ Utiliser la prop directement
const UserProfile = ({ user }: { user: User }) => {
return <p>{user.name}</p>
}Exception : formulaire d'édition
Le seul cas valide est un formulaire où l'utilisateur modifie une copie locale avant de sauvegarder :
// ✅ Exception : copie locale intentionnelle pour un formulaire d'édition
const EditUserForm = ({ user }: { user: User }) => {
const [draft, setDraft] = useState(user.name)
// Réinitialiser si l'utilisateur source change
// (voir section 5 pour une meilleure approche avec `key`)
return (
<form>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<button type="submit">Sauvegarder</button>
</form>
)
}Dans ce cas, la divergence est intentionnelle : l'utilisateur modifie un brouillon local.
4. Gérer les événements utilisateur dans un useEffect
Le problème
Réagir à une action utilisateur (clic, soumission) via useEffect au lieu d'un handler direct. L'intention est perdue et le code devient difficile à suivre.
// ❌ Anti-pattern : événement utilisateur géré dans useEffect
const ExportButton = ({ reportId }: { reportId: string }) => {
const [shouldExport, setShouldExport] = useState(false)
useEffect(() => {
if (shouldExport) {
fetch("/api/reports/export", {
method: "POST",
body: JSON.stringify({ reportId }),
})
setShouldExport(false)
}
}, [shouldExport, reportId])
return <button onClick={() => setShouldExport(true)}>Exporter</button>
}Problèmes :
- L'appel API est déclenché par un changement de state, pas par l'événement
- Si
reportIdchange pendant queshouldExportesttrue, l'effet se ré-exécute et exporte le mauvais rapport - Le flag
shouldExportn'a aucune raison d'exister
La correction
L'action dans le handler, directement. Pour la gestion d'erreurs dans ces handlers, le Result Pattern offre une alternative explicite au try/catch.
// ✅ Action dans le handler
const ExportButton = ({ reportId }: { reportId: string }) => {
const handleExport = async () => {
await fetch("/api/reports/export", {
method: "POST",
body: JSON.stringify({ reportId }),
})
}
return <button onClick={handleExport}>Exporter</button>
}La documentation React est explicite : les effets servent à synchroniser avec des systèmes externes (DOM, API tierces, WebSockets). Un clic utilisateur n'est pas un système externe.
Règle de décision
| Déclencheur | Où mettre la logique |
|---|---|
| Action utilisateur (clic, soumission, keypress) | Event handler |
| Changement de props/state qui doit se synchroniser avec un système externe | useEffect |
| Montage/démontage du composant (setup/cleanup) | useEffect |
5. Réinitialiser le state avec useEffect sur les props
Le problème
Quand un composant reçoit un nouvel ID ou une nouvelle entité, le state local doit être réinitialisé. Le réflexe courant est d'utiliser un useEffect :
// ❌ Anti-pattern : réinitialiser le state via useEffect
const CommentForm = ({ postId }: { postId: string }) => {
const [comment, setComment] = useState("")
useEffect(() => {
setComment("")
}, [postId])
return (
<form>
<textarea value={comment} onChange={(e) => setComment(e.target.value)} />
<button type="submit">Commenter</button>
</form>
)
}Même problème que l'anti-pattern #1 : deux renders (un avec l'ancien commentaire, un après le reset). Le formulaire affiche brièvement le texte du post précédent.
La correction
La prop key de React force le démontage et le remontage du composant, réinitialisant tout son state interne :
// ✅ Utiliser key pour réinitialiser le state
const PostPage = ({ postId }: { postId: string }) => {
return (
<div>
<PostContent postId={postId} />
<CommentForm key={postId} />
</div>
)
}
const CommentForm = () => {
const [comment, setComment] = useState("")
return (
<form>
<textarea value={comment} onChange={(e) => setComment(e.target.value)} />
<button type="submit">Commenter</button>
</form>
)
}Quand postId change, React démonte l'ancien CommentForm et en monte un nouveau avec un state vierge. Un seul render, pas de flash.
Le composant CommentForm n'a plus besoin de connaître postId. Sa responsabilité est réduite (principe SRP).
Contrepartie : key force le démontage et le remontage du sous-arbre DOM complet. Pour un formulaire simple, le coût est négligeable. Pour un composant avec un DOM lourd (éditeur riche, tableau complexe), mesurer avec le React DevTools Profiler avant d'adopter ce pattern.
6. Chaîner les useEffect pour synchroniser des states
Le problème
Plusieurs useEffect qui se déclenchent en cascade, chaque setter en déclenchant un autre :
// ❌ Anti-pattern : cascade de useEffect
const ShippingForm = () => {
const [country, setCountry] = useState("FR")
const [city, setCity] = useState("")
const [zipCode, setZipCode] = useState("")
// Quand le pays change, réinitialiser la ville
useEffect(() => {
setCity("")
}, [country])
// Quand la ville change, réinitialiser le code postal
useEffect(() => {
setZipCode("")
}, [city])
return (
<form>
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="FR">France</option>
<option value="BE">Belgique</option>
</select>
<input
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Ville"
/>
<input
value={zipCode}
onChange={(e) => setZipCode(e.target.value)}
placeholder="Code postal"
/>
</form>
)
}Un changement de pays provoque trois renders : country change, city se réinitialise, zipCode se réinitialise. Chaque useEffect ajoute un render supplémentaire.
La correction
Réinitialiser dans le handler d'événement, là où le changement est initié :
// ✅ Réinitialisation dans le handler
const ShippingForm = () => {
const [country, setCountry] = useState("FR")
const [city, setCity] = useState("")
const [zipCode, setZipCode] = useState("")
const handleCountryChange = (newCountry: string) => {
setCountry(newCountry)
setCity("")
setZipCode("")
}
const handleCityChange = (newCity: string) => {
setCity(newCity)
setZipCode("")
}
return (
<form>
<select
value={country}
onChange={(e) => handleCountryChange(e.target.value)}
>
<option value="FR">France</option>
<option value="BE">Belgique</option>
</select>
<input
value={city}
onChange={(e) => handleCityChange(e.target.value)}
placeholder="Ville"
/>
<input
value={zipCode}
onChange={(e) => setZipCode(e.target.value)}
placeholder="Code postal"
/>
</form>
)
}Un seul render par changement. La logique de réinitialisation est explicite et localisée.
Les erreurs à éviter
Récapitulatif des signaux d'alerte dans votre codebase :
| Signal d'alerte | Anti-pattern probable | Section |
|---|---|---|
useEffect + setState avec une valeur calculée à partir des deps | État dérivé (#1) | 1 |
useState pour une valeur qui n'apparaît pas dans le JSX | useState au lieu de useRef (#2) | 2 |
useState(props.something) dans l'initialisation | Props copiées dans le state (#3) | 3 |
Un flag boolean shouldDoX qui déclenche un useEffect | Événement dans useEffect (#4) | 4 |
useEffect(() => { setState("") }, [someId]) | Reset via useEffect au lieu de key (#5) | 5 |
Plusieurs useEffect dont les deps se chevauchent | Cascade de synchronisation (#6) | 6 |
useEffect qui appelle un callback parent (onDataChange(data)) | Remontée de state/data vers le parent |
Autres anti-patterns à connaître
Les six anti-patterns ci-dessus couvrent les cas les plus fréquents, mais d'autres patterns useEffect méritent attention :
| Anti-pattern | Problème | Correction |
|---|---|---|
Remonter du state vers le parent via callback dans useEffect | Flux de données bidirectionnel, re-renders en cascade | Remonter le state dans le parent (lift state up) |
Fetch dans l'enfant puis remontée via useEffect + callback | L'enfant contrôle des données qui ne lui appartiennent pas | Déplacer le fetch dans le parent et passer les données en props |
Passer une ref au parent via useEffect | Fuite de détails d'implémentation entre composants | Accepter ref comme prop (React 19+) |
Le plugin eslint-plugin-react-you-might-not-need-an-effect détecte automatiquement ces patterns (no-pass-live-state-to-parent, no-pass-data-to-parent, no-pass-ref-to-parent).
FAQ
useEffect est-il à éviter complètement ?
Non. useEffect reste nécessaire pour synchroniser avec des systèmes externes : connexion WebSocket, abonnement à un event bus, intégration d'une librairie non-React (carte, éditeur), setup/cleanup au montage. La règle est simple : si la logique ne concerne pas un système externe, il existe probablement une alternative sans useEffect. La documentation React détaille les cas légitimes.
Le React Compiler rend-il useMemo inutile ?
Dans la plupart des cas, oui. Le React Compiler (v1.0 stable depuis octobre 2025, opt-in dans Next.js 16 via reactCompiler: true) applique automatiquement la mémoïsation au build. Les useMemo et useCallback explicites deviennent redondants pour le nouveau code. En revanche, les anti-patterns décrits dans cet article (état dérivé dans useEffect, props dans le state) ne sont pas corrigés par le Compiler. Ce sont des erreurs de logique, pas d'optimisation.
Comment détecter ces anti-patterns dans une codebase existante ?
Trois approches complémentaires : les règles ESLint décrites dans la section 1 (set-state-in-effect du plugin officiel, eslint-plugin-react-you-might-not-need-an-effect communautaire) détectent automatiquement les cas les plus courants. Les composants avec 3+ useEffect sont les premiers candidats à un refactoring.
Ces corrections s'appliquent-elles avec React Server Components ?
Oui, et elles deviennent encore plus importantes. Les Server Components exécutent le render côté serveur, où useEffect ne s'exécute pas. Migrer vers du calcul direct (anti-pattern #1) produit des fonctions pures sans hooks, ce qui facilite leur extraction vers des Server Components. En revanche, les handlers (anti-pattern #4) restent des fonctionnalités client : un composant avec onClick nécessite 'use client' au même titre qu'un composant avec useEffect. Le vrai critère est l'absence totale d'interactivité (pas de hooks, pas de handlers, pas d'API navigateur).
Faut-il refactorer tous les useEffect d'un coup ?
Non. Prioriser par impact : commencer par les composants les plus re-rendus (React DevTools Profiler), puis les composants avec le plus de useEffect. Un composant avec un seul useEffect pour du data fetching fonctionne correctement. Les cascades (anti-pattern #6) et les états dérivés (anti-pattern #1) sont les corrections les plus rentables. Les composants sans useEffect sont aussi plus simples à tester.
Conclusion
Points clés
- L'état dérivé est l'anti-pattern le plus fréquent et le plus facile à corriger : remplacer
useEffect+setStatepar un calcul direct - useState vs useRef : si la valeur ne s'affiche pas, c'est un
useRef - Les événements utilisateur vont dans les handlers, pas dans les
useEffect - La prop
keyréinitialise le state sansuseEffect - Le React Compiler automatise
useMemo/useCallback, mais ne corrige pas les erreurs de logique
Checklist de détection rapide
- Rechercher les
useEffectqui appellent un setter avec une valeur dérivée des deps - Vérifier que les
useStatestockent des valeurs affichées dans le JSX - Identifier les composants avec 3+
useEffect - Vérifier l'absence de props copiées dans le state (
useState(props.x)) - Chercher les
useEffectqui appellent un callback parent avec du state local

À propos de l'auteur
Je suis Dimitri Dumont, développeur freelance spécialisé React & Next.js depuis plus de 7 ans. J'ai accompagné 22 startups et réalisé 43 missions avec une note de 5/5. J'applique ces patterns au quotidien pour continuer de livrer rapidement du code évolutif. En savoir plus →
Cet article vous a été utile ?
Je peux vous accompagner sur votre projet React & Next.js.
Discutons de votre projet →