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...
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 !
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;
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.
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
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
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;