Aller au contenu principal

Les erreurs courantes en débutant avec React

· 10 minutes de lecture
Damien Buchet

Dans cet article, je vais passer en revue les 5 erreurs les plus courantes que je vois passer sur StackOverflow. Ainsi que quelques conseils, qui s'appliquent certes aux débutant en React, mais aussi à tout développeur débutant 😉

Quelques conseils

Ces conseils vont vous sembler dérisoires tellement ils sont évident. Et pourtant, combien de posts sur Stackoverlow n'existeraient pas si l'auteur avait suivi ces quelques conseils...

RTFM !

Alors oui, on attaque un peu agressif. Mais les documentations ne sont pas là (uniquement) pour faire joli. Quand vous achetez une machine à laver, un four, vous lisez le manuel d'utilisation, vous ne branchez pas "au hasard" et en réglant "au hasard" le programmateur.

C'est pareil avec n'importe quelle librairie / framework.

La plupart des documentations regorgent d'exemples et d'exercices. Faites les !

Je vous conseille également de lire ces documentations en anglais. Les traductions sont trop approximatives. Et si vous ne comprenez pas un passage, utilisez GoogleTrad ou Deepl uniquement sur des courtes phrases. Ne traduisez jamais toute la page !

Lisez les messages d'erreur !

J'ai déjà eu l'occasion d'en parler, les messages d'erreur regorgent d'informations !

Si des développeurs prennent la peine de faire des messages d'erreurs lisible,... c'est pour qu'ils soient lu ! Et parfois même vous aurez la résolution possible directement dans ce message !

Une fois ceci dit, attaquons maintenant les cas d'erreurs les plus courant trouvés sur StackOverflow

"setState ne marche pas"

Premier cas, setState ne fonctionnerait pas, car quand je fais un console.log ca m'affiche la précédente valeur du state, et pas celle que je viens de mettre dans le state avec setState. Et si je reclique, ca m'affiche bien la bonne valeur.

import React, { useState } from 'react';

const App = () => {
const [state, setState] = useState('My Initial Value');

const onClick = () => {
setState('My New Value');
console.log(state);
};

return <button onClick={onClick}>Click me</button>;
};

export default App;
Pourquoi ?

Parce que setState est ASYNCHRONE

Concrètement, ca veut dire que l'execution effective du setState se fera après l'exécution des instructions en cours. Donc quand vous faites votre console.log(state), le state n'a pas encore été modifié par votre setState.

Pourquoi ce comportement ? Plusieurs réponses à ca. La modification d'un state est censé provoquer un re-render du composant, ce qui veut dire que si le setState était synchrone, vous bloqueriez la suite de l'execution de votre fonction jusqu'à ce que le composant ai terminé son re-render. Ce qui peut prendre un temps indeterminé.

React va également optimiser les setState successifs pour les regrouper et ne faire qu'un seul re-render.

import React, { useState } from 'react';

const App = () => {
const [state, setState] = useState('My Initial Value');

const onClick = () => {
setState('My New Value');
setState('My Other Updated Value');
};

console.log(state);

return <button onClick={onClick}>Click me</button>;
};

export default App;

Dans cet exemple, j'appelle 2 setState successif. On pourrait s'attendre à avoir dans la console My Initial Value > My New Value > My Other Updated Value mais nous avons bien uniquement My Initial Value > My Other Updated Value. Car React a groupé les setState successif pour ne faire qu'un seul re-render.

import React, { useState } from 'react';

const App = () => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

const onClick = () => {
setFirstName('Damien');
setLastName('Buchet');
setEmail('[email protected]');
};

console.log("I'm rendered!", firstName, lastName, email);

return <button onClick={onClick}>Click me</button>;
};

export default App;

De la même façon ici, bien que je fasse trois setState différents de suite, React ne va provoquer qu'un seul re-render.

Bon à savoir

Il existe un cas où React ne peut pas regrouper les setState : après avoir await une exécution asynchrone. Par exemple :

import React, { useState } from 'react';

const App = () => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

const onClick = async () => {
setFirstName('Damien');
setLastName('Buchet');

// Do an asynchronous "pause" of 1.5 seconds
await new Promise(resolve => setTimeout(() => resolve(), 1500));

setFirstName('Damien-Updated');
setLastName('Buchet-Updated');
setEmail('[email protected]');
};

console.log("I'm rendered!", firstName, lastName, email);

return <button onClick={onClick}>Click me</button>;
};

export default App;

Les deux setState avant le await vont bien être groupés pour ne faire qu'un seul re-render. Par contre après le await, chaque setState va générer un re-render.

IMPORTANT : Ce fonctionnement sera corrigé avec React 18, et les setState suite à une exécution asynchrone seront désormais bien groupés

L'état initial du state

Cannot read properties of undefined (reading 'map')

import React, { useState, useEffect } from 'react';

const App = () => {
const [rows, setRows] = useState();

useEffect(() => {
const getDatas = async () => {
// we simulate an API return after 1.5 secondes
const res = await new Promise(resolve => {
setTimeout(() => {
resolve([
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]);
}, 1500);
});
setRows(res);
};

getDatas();
}, []);

return (
<div>
{rows.map((r, i) => (
<div key={i}>{r}</div>
))}
</div>
);
};

export default App;

Comme vous le voyez, nous avons une erreur ici. Cela vient de l'état initial de notre state.

Il faut bien avoir en tête que useEffect se déclenche après que le composant ait été monté. Ce qui veut dire que le premier render du composant va être effectué avec l'état initial du state.

Ici nous avons const [rows, setRows] = useState() ce qui veut dire que le state n'est pas initialisé, et est donc undefined. Et donc au premier render, nous essayons de faire un undefined.map ce qui fait crasher notre application.

Pour corriger ce problème, le plus simple est d'initialiser votre state avec un tableau vide

import React, { useState, useEffect } from 'react';

const App = () => {
const [rows, setRows] = useState([]);

useEffect(() => {
const getDatas = async () => {
// we simulate an API return after 1.5 secondes
const res = await new Promise(resolve => {
setTimeout(() => {
resolve([
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]);
}, 1500);
});
setRows(res);
};

getDatas();
}, []);

return (
<div>
{rows.map((r, i) => (
<div key={i}>{r}</div>
))}
</div>
);
};

export default App;

Vous pouvez également utiliser l'optional chaining en faisant rows?.map. Cela aura pour effet de stopper l'exécution si row est undefined ou null, et donc ne pas exécuter le .map

import React, { useState, useEffect } from 'react';

const App = () => {
const [rows, setRows] = useState([]);

useEffect(() => {
const getDatas = async () => {
// we simulate an API return after 1.5 secondes
const res = await new Promise(resolve => {
setTimeout(() => {
resolve([
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]);
}, 1500);
});
setRows(res);
};

getDatas();
}, []);

return (
<div>
{rows?.map((r, i) => (
<div key={i}>{r}</div>
))}
</div>
);
};

export default App;

Re-render infinis

Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop

import React, { useState, useEffect } from 'react';

const App = () => {
const [rows, setRows] = useState([
'[email protected]',
'[email protected]',
'[email protected]',
]);

useEffect(() => {
const getDatas = async () => {
// we simulate an API return after 300 ms
const res = await new Promise(resolve => {
setTimeout(() => {
resolve(['[email protected]', '[email protected]']);
}, 300);
});
setRows([...rows, ...res]);
};

getDatas();
}, [rows]);

return (
<div>
{rows.map((r, i) => (
<div key={i}>{r}</div>
))}
</div>
);
};

export default App;

J'aurais aimé vous mettre le render ici, mais CodeSandBox ne semble pas avoir de problème avec les infinite loop 😂 Mais vous pouvez tester ce code chez vous, et vous aurez cette erreur.

Quel est le problème ici ? Et bien vous avez, dans le tableau de dépendance du useEffect, la valeur du state qui est elle-même modifiée dans le useEffect.

Donc à chaque fois que useEffect est appelé, vous faites un setState, qui va changer le state, qui va lancer le useEffect, qui va changer le state, qui va lancer le useEffect,... Infinite loop

Alors comment gérer ce problème ? Et bien il ne faut JAMAIS avoir dans le tableau de dépendance d'un useEffect, un state qui sera changé dans le corps du useEffect. JAMAIS !

Oui, mais parfois, j'en ai quand meme besoin, alors comment je peux faire ? JAMAIS ! Vous n'avez pas besoin du state, vous avez besoin de la valeur du state au moment de la modification, et pour ca, setState peut également prendre en paramètre non pas le nouvel état du state directement, mais un fonction, qui aura en paramètre la valeure actuelle du state, et le state sera mis à jour avec le retour de cette fonction. Donc dans notre exemple précédent :

import React, { useState, useEffect } from 'react';

const App = () => {
const [rows, setRows] = useState([
'[email protected]',
'[email protected]',
'[email protected]',
]);

useEffect(() => {
const getDatas = async () => {
// we simulate an API return after 300 ms
const res = await new Promise(resolve => {
setTimeout(() => {
resolve(['[email protected]', '[email protected]']);
}, 300);
});
setRows(state => [...state, ...res]);
};

getDatas();
}, []);

return (
<div>
{rows.map((r, i) => (
<div key={i}>{r}</div>
))}
</div>
);
};

export default App;

Du coup notre rows n'est plus dans le tableau de dépendance de notre useEffect, et tout fonctionne sans problème !

Le composant ne s'affiche pas

Objects are not valid as a React child (found: object with keys { ... }). If you meant to render a collection of children, use an array instead.

import React, { useState, useEffect } from 'react';

const App = () => {
const [rows, setRows] = useState([]);

useEffect(() => {
const getDatas = async () => {
// we simulate an API return after 1.5 secondes
const res = await new Promise(resolve => {
setTimeout(() => {
resolve([
{ name: 'Foo', email: '[email protected]' },
{ name: 'John', email: '[email protected]' },
{ name: 'Hello', email: '[email protected]' },
{ name: 'Toto', email: '[email protected]' },
{ name: 'Michel', email: '[email protected]' },
]);
}, 1500);
});
setRows(res);
};

getDatas();
}, []);

return (
<div>
{rows.map((r, i) => (
<div key={i}>{r}</div>
))}
</div>
);
};

export default App;

Ici, on stocke dans notre state le retour de l'api qui est donc un tableau d'objets. OR, dans notre JSX, on essaye d'afficher directement rows[i], donc un Object, ce qui n'est pas possible. Il faudra donc afficher les valeurs des attributs / propriétés de l'objet dans le JSX et non pas directement l'objet lui-meme.

import React, { useState, useEffect } from 'react';

const App = () => {
const [rows, setRows] = useState([]);

useEffect(() => {
const getDatas = async () => {
// we simulate an API return after 1.5 secondes
const res = await new Promise(resolve => {
setTimeout(() => {
resolve([
{ name: 'Foo', email: '[email protected]' },
{ name: 'John', email: '[email protected]' },
{ name: 'Hello', email: '[email protected]' },
{ name: 'Toto', email: '[email protected]' },
{ name: 'Michel', email: '[email protected]' },
]);
}, 1500);
});
setRows(res);
};

getDatas();
}, []);

return (
<div>
{rows.map((r, i) => (
<div key={i}>{r.name} - {r.email}</div>
))}
</div>
);
};

export default App;

Les keys

A quoi servent les key que React nous "oblige" à mettre dans des map par exemple ? Cela va servir à React d'identifiant unique lors de re-render successif, et ainsi permettre de conserver l'état du composant.

Alors que se passe-t-il si j'utilise tojours l'index du map pour me servir de key ? Si vos composants n'ont pas de state interne, il est fort peu probable que vous ayez des effets de bord. Par contre quand vos composants ont un state interne,... Prenons l'exemple suivant :

import React, { useState } from 'react';

// We create an array of 10 different colors
const colorArray = [ '#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6', '#E6B333', '#3366E6', '#999966', '#99FF99', '#B34D4D' ];
// We generate an array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const array = Array.from({ length: 10 }).map((_, i) => i);

const Counter = ({ id }) => {
const [count, setCount] = useState(id + 1);
const [style] = useState({ backgroundColor: colorArray[id] });

return (
<button onClick={() => setCount(c => c + 1)} style={style}>
[{id}] Inc Me! ({count})
</button>
);
};

const App = () => {
const [buttons, setButtons] = useState([...array]);

const onRemoveButton = () =>
setButtons(state => state.filter(index => index !== 4));

return (
<>
<div>
{buttons.map((id, i) => (
<Counter key={i} id={id} />
))}
</div>
<div style={{ marginTop: 10 }}>
<button onClick={onRemoveButton}>
Click me to remove button id 4
</button>
</div>
</>
);
};

export default App;

Dans ce code, nous générons 10 boutons, qui ont leur propre compteur interne, et une couleur de background qui correspond à leur index. En cliquant sur le bouton Click me to remove button index 4, on va supprimer dans notre tableau l'id 4. On devrait donc s'attendre à voir le bouton bleu ciel disparaitre, et pourtant c'est le dernier élément du tableau qui disparait ! Encore plus etonnant, l'id du bouton blue ciel (affiché entre []) passe de 4 à 5.

Alors pourquoi ca. Ca vient précisement des key. Au premier render, React va associe chaque composant avec une key. Donc au premier render, on va avoir

Id Array    Index map       key         backgroundColor
0 0 0 #FF6633
1 1 1 #FFB399
2 2 2 #FF33FF
3 3 3 #FFFF99
4 4 4 #00B3E6
5 5 5 #E6B333
6 6 6 #3366E6
7 7 7 #999966
8 8 8 #99FF99
9 9 9 #B34D4D

Au 2ème render, nous avons donc supprimé l'id 4. Mais à ce moment React va se baser sur la key pour ré-utiliser le composant qui avait cette key au précédent render. Avec la suppression de cet id, nous avons le tableau de correspondance suivant :

Id Array    Index map       key         backgroundColor
0 0 0 #FF6633
1 1 1 #FFB399
2 2 2 #FF33FF
3 3 3 #FFFF99
5 4 4 #00B3E6
6 5 5 #E6B333
7 6 6 #3366E6
8 7 7 #999966
9 8 8 #99FF99

L'id du tableau 5, correspond maintenant à l'index 4 du tableau, et donc à la key 4. Et la key 4 du précédent render correspond au backgroundColor === '#00B3E6'. Le state correspond donc au composant de key = 4. Et comme l'id est passé en prop, celui ci sera updaté dans le composant, avec le mauvais state.

Alors comment régler ce problème ? Et bien chacunes de vos key doivent être unique et être persistantes lors des re-render successifs. Et nous avons justement un id "unique" ici, c'est l'id qui est stocké dans notre tableau. Il suffira alors d'utiliser cette valeur comme key

import React, { useState } from 'react';

// We create an array of 10 different colors
const colorArray = [ '#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6', '#E6B333', '#3366E6', '#999966', '#99FF99', '#B34D4D' ];
// We generate an array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const array = Array.from({ length: 10 }).map((_, i) => i);

const Counter = ({ id }) => {
const [count, setCount] = useState(id + 1);
const [style] = useState({ backgroundColor: colorArray[id] });

return (
<button onClick={() => setCount(c => c + 1)} style={style}>
[{id}] Inc Me! ({count})
</button>
);
};

const App = () => {
const [buttons, setButtons] = useState([...array]);

const onRemoveButton = () =>
setButtons(state => state.filter(index => index !== 4));

return (
<>
<div>
{buttons.map(id => (
<Counter key={id} id={id} />
))}
</div>
<div style={{ marginTop: 10 }}>
<button onClick={onRemoveButton}>
Click me to remove button id 4
</button>
</div>
</>
);
};

export default App;

Au premier render, nous avons :

Id Array    Index map       key         backgroundColor
0 0 0 #FF6633
1 1 1 #FFB399
2 2 2 #FF33FF
3 3 3 #FFFF99
4 4 4 #00B3E6
5 5 5 #E6B333
6 6 6 #3366E6
7 7 7 #999966
8 8 8 #99FF99
9 9 9 #B34D4D

Et suite au clic qui supprime le bouton, nous nous retrouvons avec

Id Array    Index map       key         backgroundColor
0 0 0 #FF6633
1 1 1 #FFB399
2 2 2 #FF33FF
3 3 3 #FFFF99
5 4 5 #E6B333
6 5 6 #3366E6
7 6 7 #999966
8 7 8 #99FF99
9 8 9 #B34D4D
Utilisez les ID de la base de données !

Ce cas est fréquent lorsqu'on récupère des informations de la base de données, pour les afficher ensuite.

Utilisez toujours l'ID de votre base de données, retourné par votre API, pour définir vos key

Attention !

Il est fortement déconseillé d'utiliser des key "random". Si une key n'existe pas entre 2 re-render, React va simplement remonter un nouveau composant à chaque fois. Vous perdrez alors toute trace de votre state interne. Par exemple, cliquez ici sur quelques boutons pour incrémenter les compteurs, puis cliquez sur le bouton pour supprimer un bouton, tous vos compteurs ont été réinitialisé. Parce que vous générez de nouvelles key à chaque re-render, et donc React monte de nouveaux composants, avec leur state initiaux.

import React, { useState } from 'react';

// We generate an array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const array = Array.from({ length: 10 }).map((_, i) => i);

const Counter = ({ id }) => {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(c => c + 1)}>
[{id}] Inc Me! ({count})
</button>
);
};

const App = () => {
const [buttons, setButtons] = useState([...array]);

const onRemoveButton = () =>
setButtons(state => state.filter(index => index !== 4));

return (
<>
<div>
{buttons.map(id => (
<Counter key={Math.random()} id={id} />
))}
</div>
<div style={{ marginTop: 10 }}>
<button onClick={onRemoveButton}>
Click me to remove button id 4
</button>
</div>
</>
);
};

export default App;