web-dev-qa-db-fra.com

Etat non mis à jour lors de l'utilisation du hook d'état React dans setInterval

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>

12
Yangshun Tay

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>

34
Yangshun Tay

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.

2
estus

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>

1
Bear-Foot