J'essaie le nouveau React Hooks et possède un composant Clock avec un compteur censé augmenter chaque seconde. Cependant, la valeur n'augmente pas au-delà de un.
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
La raison en est que le rappel passé dans la fermeture de setInterval
accède uniquement à la variable time
dans le premier rendu, il n'a pas accès à la nouvelle valeur time
dans le rendu suivant car le useEffect()
n'est pas appelé la deuxième fois.
time
a toujours la valeur 0 dans le rappel setInterval
.
Comme pour setState
que vous connaissez bien, les crochets d’état ont deux formes: l’une dans l’état mis à jour et le formulaire de rappel dans lequel l’état actuel est transmis. Vous devez utiliser le deuxième formulaire et lire la dernière valeur d’état dans la setState
rappel pour vous assurer que vous avez la dernière valeur d'état avant de l'incrémenter.
Bonus: Approches alternatives
Dan Abramov, approfondit le sujet de l'utilisation de
setInterval
avec des crochets dans son blog post et propose d'autres moyens de contourner ce problème. Je recommande vivement de le lire!
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(prevTime => prevTime + 1); // <-- Change this line!
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
La fonction useEffect
est évaluée une seule fois lors du montage du composant lorsqu'une liste d'entrées vide est fournie.
Une alternative à setInterval
consiste à définir un nouvel intervalle avec setTimeout
chaque fois que l'état est mis à jour:
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = setTimeout(() => {
setTime(time + 1);
}, 1000);
return () => {
clearInterval(timer);
};
}, [time]);
L'impact de setTimeout
sur les performances est insignifiant et peut généralement être ignoré. Sauf si le composant est sensible au temps et au point où les délais d'attente nouvellement définis entraînent des effets indésirables, les deux approches setInterval
et setTimeout
sont acceptables.
Une autre solution serait d'utiliser useReducer
, car l'état actuel sera toujours transmis.
function Clock() {
const [time, dispatch] = React.useReducer((state = 0, action) => {
if (action.type === 'add') return state + 1
return state
});
React.useEffect(() => {
const timer = window.setInterval(() => {
dispatch({ type: 'add' });
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>