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

13 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 setter

L'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 use

EffectSans 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 use

Effect + 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 use

Effect 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 use

EffectTimers, 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 use

EffectuseEffect 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 set

State 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 use

EffectLes 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 merger

Avec 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 updates

Le 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 <input value={name} onChange={e =

> setName(e.target.value)} />;}// ✅ Utilise key pour forcer le remontage quand initialName change<UserForm key={userId} initialName={initialName} />// ✅ 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édiaire

Tu 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 (    <ul

>      {todos.map((todo, index) =

> (        <li key={index}

>          <input defaultValue={todo.text} /

>          <button onClick={() =

> onDelete(todo.id)}>Supprimer</button

>        </li

>      ))}    </ul

>  );}// ✅ Utiliser un identifiant stable et uniquefunction TodoList({ todos, onDelete }) {  return (    <ul

>      {todos.map((todo) =

> (        <li key={todo.id}

>          <input defaultValue={todo.text} /

>          <button onClick={() =

> onDelete(todo.id)}>Supprimer</button

>        </li

>      ))}    </ul

>  );}

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 <button onClick={increment}>{count}</button>;}// ✅ 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 <button onClick={increment}>{count}</button>;}

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 (    <AppContext.Provider value={{ user, theme, notifications, setUser, setTheme }}

>      {children}    </AppContext.Provider

>  );}// ThemeToggle re-rend quand notifications change aussi !function ThemeToggle() {  const { theme, setTheme } = useContext(AppContext);  return <button onClick={() =

> setTheme(t =

> t === 'light' ? 'dark' : 'light')}>{theme}</button>;}


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 <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;}// Maintenant ThemeToggle ne re-rend que quand theme changefunction ThemeToggle() {  const { theme, setTheme } = useContext(ThemeContext);  return <button onClick={() =

> setTheme(t =

> t === 'light' ? 'dark' : 'light')}>{theme}</button>;}

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 fallback={<Spinner />}

>      <Sidebar /

>      <Feed /

>      <Recommendations /

>    </Suspense

>  );}// ✅ Suspense granulaire — chaque zone charge indépendammentfunction App() {  return (    <div className="layout"

>      <Sidebar /

>      <Suspense fallback={<FeedSkeleton />}

>        <Feed /

>      </Suspense

>      <Suspense fallback={<RecoSkeleton />}

>        <Recommendations /

>      </Suspense

>    </div

>  );}

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'Error

Boundary avec le hook use() (React 19)


jsx// ❌ Si l'API échoue → écran blanc, aucun messagefunction UserProfile({ userPromise }) {  const user = use(userPromise);  return <h1>{user.name}</h1>;}function App() {  return (    <Suspense fallback={<p>Chargement...</p>}

>      <UserProfile userPromise={fetchUser(42)} /

>    </Suspense

>  );}// ✅ Toujours associer Suspense + ErrorBoundaryfunction App() {  return (    <ErrorBoundary fallback={<p>Erreur de chargement</p>}

>      <Suspense fallback={<p>Chargement...</p>}

>        <UserProfile userPromise={fetchUser(42)} /

>      </Suspense

>    </ErrorBoundary

>  );}

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 []</code

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</code

ou dépendances primitives

9Écraser le state objetSpread ...prev</code

ou useReducer

10State initialisé avec propsUtiliser key</code

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 à retenir

Ces 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.


Pour revoir la syntaxe correcte de ces hooks depuis le début, consulte notre guide pratique sur useState et useEffect — avec tous les patterns et les exemples concrets.

Envie de pratiquer ? GoGoKodo propose des ateliers interactifs pour apprendre React, JavaScript et Python en codant directement dans le navigateur — 100% gratuit.