15 erreurs React que tous les débutants font (et comment les éviter)

12 min de lecture React

Tu débutes avec React et tes composants ne marchent pas comme prévu ? Pas de panique. Si tu n'as pas encore franchi le pas, commence par notre guide pour débuter avec React qui couvre l'installation et les bases. Sinon, avant de plonger dans React, assure-toi de connaître les erreurs courantes en JavaScript — beaucoup de bugs React viennent en réalité de JavaScript. Voici les 10 erreurs les plus fréquentes que font les débutants avec React — et 5 bonus pour les développeurs intermédiaires. Chaque erreur est illustrée avec du code avant/après.## 1. Modifier le state directement au lieu d'utiliser le setterL'erreur la plus classique. Tu modifies une variable d'état directement au lieu de passer par la fonction setter.``jsx// ❌ Mauvais — React ne détecte pas le changementconst [user, setUser] = useState({ name: 'Ali', age: 25 });user.name = 'Sara'; // Rien ne se passe à l'écran// ✅ Correct — créer un nouvel objetsetUser({ ...user, name: 'Sara' });`Pourquoi ? React compare les références avec Object.is(). Si tu mutes l'objet existant, la référence reste la même et React ne re-rend pas le composant. C'est le principe d'immutabilité : on ne modifie jamais, on remplace. Si les notions de const et de référence te semblent floues, révise d'abord les bases des variables JavaScript.Même piège avec les tableaux :`jsx// ❌ Mauvais — push mute le tableauconst [items, setItems] = useState(['React', 'Vue']);items.push('Angular'); // La référence ne change pas// ✅ Correct — créer un nouveau tableausetItems([...items, 'Angular']);`## 2. Oublier le tableau de dépendances de useEffectSans tableau de dépendances, ton effet s'exécute à chaque rendu. Avec un tableau vide [], il s'exécute une seule fois au montage.`jsx// ❌ S'exécute à CHAQUE rendu — catastrophe avec un fetchuseEffect(() => { fetch('/api/data').then(r => r.json()).then(setData);});// ✅ S'exécute une seule fois au montageuseEffect(() => { fetch('/api/data').then(r => r.json()).then(setData);}, []);// ✅ S'exécute quand userId changeuseEffect(() => { fetch(/api/users/${userId}).then(r => r.json()).then(setUser);}, [userId]);`Règle d'or : mets toujours un tableau de dépendances. Ajoute-y toutes les variables utilisées dans l'effet. Le plugin ESLint react-hooks/exhaustive-deps te prévient si tu oublies une dépendance.## 3. Créer des boucles infinies avec useEffect + setStateC'est le piège classique : ton useEffect met à jour un state qui est dans ses dépendances.`jsx// ❌ Boucle infinie : setCount → re-rendu → useEffect → setCount → ...const [count, setCount] = useState(0);useEffect(() => { setCount(count + 1);}, [count]);// ✅ Utilise la forme fonctionnelle pour un timeruseEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); }, 1000); return () => clearInterval(timer);}, []); // Pas de dépendance sur count`Comment la détecter ? Si ta console affiche des centaines de logs en boucle ou si ton navigateur ralentit soudainement, c'est probablement une boucle infinie dans un useEffect.## 4. Utiliser useEffect pour du state dérivéC'est l'anti-pattern le plus fréquent selon la doc React elle-même. Si une valeur peut être calculée à partir du state ou des props, ne la mets pas dans un useEffect.`jsx// ❌ Anti-pattern : useEffect pour calculer une valeur dérivéeconst [items, setItems] = useState([]);const [total, setTotal] = useState(0);useEffect(() => { setTotal(items.reduce((sum, item) => sum + item.price, 0));}, [items]);// ✅ Calcul direct pendant le rendu — simple et performantconst [items, setItems] = useState([]);const total = items.reduce((sum, item) => sum + item.price, 0);`Pourquoi ? Le calcul direct est plus simple, plus rapide, et évite un re-rendu supplémentaire. Avec le useEffect, tu as deux rendus : un avec l'ancien total, puis un avec le nouveau. Le signal d'alerte : un useEffect qui appelle un setter du même composant avec une valeur calculée à partir de ses dépendances.## 5. Oublier le cleanup dans useEffectTimers, event listeners, abonnements WebSocket… sans cleanup, tu crées des fuites mémoire et des bugs difficiles à tracer.`jsx// ❌ Le timer continue après le démontage du composantuseEffect(() => { const timer = setInterval(() => console.log('tick'), 1000);}, []);// ✅ Nettoyer en retournant une fonctionuseEffect(() => { const timer = setInterval(() => console.log('tick'), 1000); return () => clearInterval(timer);}, []);// ✅ Pareil pour les event listenersuseEffect(() => { const handler = (e) => console.log(e.key); window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler);}, []);`La fonction retournée par useEffect est appelée quand le composant est démonté ou avant que l'effet ne se ré-exécute. En mode StrictMode (dev), React monte/démonte/remonte pour t'aider à détecter les cleanups manquants.## 6. Utiliser async directement dans useEffectuseEffect attend une fonction de cleanup (ou rien), pas une Promise. Si l'asynchrone te semble flou, commence par comprendre async/await avant de l'utiliser dans tes effets.`jsx// ❌ useEffect ne peut pas être asyncuseEffect(async () => { const res = await fetch('/api/data'); const data = await res.json(); setData(data);}, []);// ✅ Créer une fonction async à l'intérieuruseEffect(() => { async function loadData() { const res = await fetch('/api/data'); const data = await res.json(); setData(data); } loadData();}, []);// ✅ Avec AbortController pour annuler si le composant démonteuseEffect(() => { const controller = new AbortController(); async function loadData() { try { const res = await fetch('/api/data', { signal: controller.signal }); const data = await res.json(); setData(data); } catch (e) { if (e.name !== 'AbortError') console.error(e); } } loadData(); return () => controller.abort();}, []);`## 7. Penser que setState est synchronesetState ne met pas à jour la variable immédiatement. React batch (regroupe) les mises à jour pour optimiser les rendus.`jsx// ❌ count vaut toujours 0 dans les 3 appelsconst [count, setCount] = useState(0);function handleClick() { setCount(count + 1); // 0 + 1 setCount(count + 1); // 0 + 1 (pas 1 + 1 !) setCount(count + 1); // 0 + 1 (pas 2 + 1 !) console.log(count); // Affiche 0, pas 3 !}// ✅ Utilise la forme fonctionnelle pour chaînerfunction handleClick() { setCount(prev => prev + 1); // 0 → 1 setCount(prev => prev + 1); // 1 → 2 setCount(prev => prev + 1); // 2 → 3}`Astuce : la forme fonctionnelle prev => prev + 1 est la solution à 90% des problèmes liés au batching. Elle garantit que tu travailles toujours avec la dernière valeur.## 8. Mettre des objets/arrays dans les dépendances de useEffectLes objets et tableaux sont comparés par référence, pas par valeur. Un nouvel objet {} est toujours différent de l'ancien {}.`jsx// ❌ Se déclenche à chaque rendu car options est un nouvel objetconst options = { page: 1, limit: 10 };useEffect(() => { fetchData(options);}, [options]); // Nouvelle référence à chaque rendu !// ✅ Solution 1 : useMemo pour stabiliser la référenceconst options = useMemo(() => ({ page: 1, limit: 10 }), []);// ✅ Solution 2 : dépendre des valeurs primitivesuseEffect(() => { fetchData({ page, limit });}, [page, limit]);// ✅ Solution 3 : sérialiser si l'objet vient des propsconst filtersKey = JSON.stringify(filters);useEffect(() => { fetchData(filters);}, [filtersKey]);`## 9. Écraser le state objet au lieu de le mergerAvec useState, le setter remplace le state (contrairement à this.setState dans les classes qui merge automatiquement).`jsx// ❌ On perd le champ 'name'const [form, setForm] = useState({ name: 'Ali', email: 'ali@mail.com' });setForm({ email: 'new@mail.com' });// form = { email: 'new@mail.com' } — name a disparu !// ✅ Spread l'ancien statesetForm(prev => ({ ...prev, email: 'new@mail.com' }));// form = { name: 'Ali', email: 'new@mail.com' }`Pour les formulaires complexes, useReducer est souvent plus adapté que useState :`jsxconst [form, dispatch] = useReducer((state, action) => { return { ...state, [action.field]: action.value };}, { name: '', email: '', age: '' });// Usage simpledispatch({ field: 'email', value: 'new@mail.com' });`## 10. Initialiser le state avec les props sans gérer les updatesLe state est initialisé une seule fois. Si la prop change, le state ne suit pas.`jsx// ❌ Si initialName change, le state name ne se met pas à jourfunction UserForm({ initialName }) { const [name, setName] = useState(initialName); return setName(e.target.value)} />;}// ✅ Utilise key pour forcer le remontage quand initialName change// ✅ Ou synchronise avec useEffect (si vraiment nécessaire)useEffect(() => { setName(initialName);}, [initialName]);`La technique du key est la solution recommandée par la doc React. En changeant la key, React détruit l'ancien composant et en crée un nouveau avec le state fraîchement initialisé.---## Bonus : 5 erreurs de développeur intermédiaireTu maîtrises les bases ? Voici les pièges qui attendent les devs plus expérimentés.## 11. Utiliser l'index comme key dans une liste dynamique`jsx// ❌ Bug : supprimer le 1er élément => le 2ème input garde la valeur du 1erfunction TodoList({ todos, onDelete }) { return (

);}// ✅ Utiliser un identifiant stable et uniquefunction TodoList({ todos, onDelete }) { return (
    {todos.map((todo) => (
  • ))}
);}
`Pourquoi ? L'index n'est pas lié à l'identité de la donnée, mais à sa position. Quand tu supprimes, tries ou filtres, les index changent mais React pense que les éléments aux mêmes positions sont les mêmes composants. Résultat : l'état interne (valeur d'un input, focus, animation) reste associé au mauvais élément. Pire : utiliser Math.random() ou crypto.randomUUID() comme key est encore plus destructeur — ça force React à détruire et recréer TOUS les composants à chaque rendu.Règle : toujours utiliser un identifiant unique et stable lié à la donnée (id de la base, UUID, slug).## 12. Stale closure avec useCallback`jsx// ❌ Le compteur ne dépasse jamais 1function Counter() { const [count, setCount] = useState(0); const increment = useCallback(() => { setCount(count + 1); // Capture count = 0 pour toujours }, []); // Dépendance manquante ! return ;}// ✅ Utiliser la mise à jour fonctionnellefunction Counter() { const [count, setCount] = useState(0); const increment = useCallback(() => { setCount(prev => prev + 1); // Lit la vraie valeur }, []); // [] est correct ici return ;}`Explication : quand useCallback a un tableau de dépendances vide [], la fonction est créée une seule fois. La closure capture la valeur initiale de count (0) et ne la met jamais à jour. C'est le problème de "stale closure" (fermeture périmée). La mise à jour fonctionnelle prev => prev + 1 lit la valeur réelle du state au moment de l'exécution.## 13. Context API monolithique qui re-rend tout`jsx// ❌ TOUT re-rend quand une seule valeur changeconst AppContext = createContext();function AppProvider({ children }) { const [user, setUser] = useState(null); const [theme, setTheme] = useState('light'); const [notifications, setNotifications] = useState([]); return ( {children} );}// ThemeToggle re-rend quand notifications change aussi !function ThemeToggle() { const { theme, setTheme } = useContext(AppContext); return ;}````jsx// ✅ Découper en contextes séparésconst ThemeContext = createContext();const UserContext = createContext();function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const value = useMemo(() => ({ theme, setTheme }), [theme]); return {children};}// Maintenant ThemeToggle ne re-rend que quand theme changefunction ThemeToggle() { const { theme, setTheme } = useContext(ThemeContext); return ;}`Pourquoi ? Un Context monolithique qui contient tout l'état de l'app est un tueur de performance. React utilise Object.is() pour détecter les changements. Un seul changement (ex: nouvelle notification) re-rend TOUS les consommateurs du contexte. La solution : découper en petits contextes + useMemo sur la valeur du Provider.## 14. Un seul Suspense boundary pour toute l'app (React 19)`jsx// ❌ TOUT disparaît pendant que Recommendations chargefunction App() { return ( }> );}// ✅ Suspense granulaire — chaque zone charge indépendammentfunction App() { return (

}> }>

);}`Pourquoi ? Si un seul composant lent suspend, tout le contenu est remplacé par le fallback. L'utilisateur voit un spinner plein écran au lieu d'un chargement progressif. Place des Suspense boundaries granulaires autour de chaque zone qui charge indépendamment.## 15. Oublier l'ErrorBoundary avec le hook use() (React 19)`jsx// ❌ Si l'API échoue → écran blanc, aucun messagefunction UserProfile({ userPromise }) { const user = use(userPromise); return

{user.name}

;}function App() { return ( Chargement...

}>
);}// ✅ Toujours associer Suspense + ErrorBoundaryfunction App() { return ( Erreur de chargement

}> Chargement...

}>
);}
`Le hook use() de React 19 fonctionne en tandem avec Suspense ET ErrorBoundary. Si la promesse est rejetée, React cherche l'ErrorBoundary le plus proche. Sans ErrorBoundary, c'est l'écran blanc.---## Résumé en un tableau

#ErreurSolution
1Muter le state directementToujours utiliser le setter avec un nouvel objet
2Pas de tableau de dépendancesToujours mettre [] ou [deps]
3Boucle infinie useEffectUtiliser la forme fonctionnelle prev => ...
4useEffect pour du state dérivéCalculer directement pendant le rendu
5Pas de cleanupRetourner une fonction de nettoyage
6async dans useEffectCréer une fonction async interne + AbortController
7setState synchroneUtiliser prev => prev + 1
8Objets dans les dépendancesuseMemo ou dépendances primitives
9Écraser le state objetSpread ...prev ou useReducer
10State initialisé avec propsUtiliser key pour forcer le remontage
11Index comme keyUtiliser un ID stable lié à la donnée
12Stale closureMise à jour fonctionnelle dans useCallback
13Context monolithiqueDécouper en contextes séparés + useMemo
14Un seul SuspenseSuspense boundaries granulaires
15Pas d'ErrorBoundaryToujours associer Suspense + ErrorBoundary

Les 3 principes à retenirCes erreurs sont normales quand on apprend React. L'important, c'est de comprendre pourquoi elles arrivent :- Immutabilité — on ne modifie jamais, on remplace. C'est la base de la détection des changements par React.- Rendus asynchrones — le state n'est pas mis à jour instantanément. React regroupe (batch) les mises à jour pour optimiser.- useEffect = synchronisation externe — useEffect sert à synchroniser ton composant avec un système extérieur (API, DOM, timer), pas à transformer du state.Maîtrise ces 3 principes et tu éviteras 90% des bugs React.Pour approfondir, la documentation officielle React reste la référence ultime sur les hooks et les patterns modernes.Note 2026 : le React Compiler (stable depuis React 19.2) automatise la mémorisation et élimine le besoin d'écrire useMemo, useCallback et React.memo` manuellement. Mais il ne corrige pas les erreurs de logique — comprendre les principes reste essentiel.---Envie de pratiquer ? GoGoKodo propose des ateliers interactifs pour apprendre React, JavaScript et Python en codant directement dans le navigateur — 100% gratuit.