0
votes

Les hooks setInterval et React produisent des résultats inattendus

J'ai le composant suivant défini dans mon application scaffolded à l'aide de create-react:

import React, { useState } from 'react';

const Play = props => {
  const [currentSecond, setCurrentSecond] = useState(1);
  let timer;
  const setTimer = () => {
    timer = setInterval(() => {
      if (currentSecond < props.secondsPerRep) {
        setCurrentSecond(() => currentSecond + 1);
      }
    }, 1000);
  }

  return (
    <div>
      <div>
        <button onClick={setTimer}>Start</button>
        <p>{currentSecond}</p>
      </div>

    </div>
  );
}

export default Play;

Et currentSecond est mis à jour toutes les secondes jusqu'à ce qu'il atteigne les accessoires .secondsPerRep cependant si j'essaye de démarrer le setInterval à partir d'un gestionnaire de clics:

import React, { useState } from 'react';

const Play = props => {
  const [currentSecond, setCurrentSecond] = useState(1);
  let timer;
  const setTimer = () => {
    timer = setInterval(() => {
      if (currentSecond < props.secondsPerRep) {
        setCurrentSecond(() => currentSecond + 1);
      }
    }, 1000);
  }
  setTimer();
  return (
    <div>
      <div>
        <p>{currentSecond}</p>
      </div>

    </div>
  );
}

export default Play;

Puis currentSecond dans le rappel setInterval retourne toujours à la valeur initiale, c'est-à-dire 1.

Toute aide très appréciée!


0 commentaires

4 Réponses :


1
votes

Votre problème est cette ligne setCurrentSecond (() => currentSecond + 1); parce que vous n'appelez setTimer qu'une seule fois, votre intervalle sera toujours fermé au-dessus de l'initiale indiquer où currentSecond est 1.

Heureusement, vous pouvez facilement y remédier en accédant à l'état actuel réel via les arguments de la fonction que vous passez à setCurrentSecond comme setCurrentSecond (actualCurrentSecond => actualCurrentSecond + 1)

De plus, vous devez être très prudent en définissant arbitrairement des intervalles dans le corps de composants fonctionnels comme celui-ci car ils ne seront pas effacés correctement, comme si vous deviez cliquer à nouveau sur le bouton, cela commencerait un autre intervalle et ne s'effacerait pas. le précédent.

Je vous recommande de consulter ce billet de blog car il répondrait à toutes vos questions sur les intervalles + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/


0 commentaires

0
votes

C'est parce que votre code se ferme sur la valeur currentSecond du rendu avant de cliquer sur le bouton. C'est javascript ne sait pas sur les re-rendus et les hooks. Vous voulez configurer cela légèrement différemment.

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

const Play = ({ secondsPerRep }) => {
  const secondsPassed = useRef(1)
  const [currentSecond, setCurrentSecond] = useState(1);
  const [timerStarted, setTimerStarted] = useState(false)

  useEffect(() => {
    let timer;

    if(timerStarted) {
      timer = setInterval(() => {
        if (secondsPassed.current < secondsPerRep) {
          secondsPassed.current =+ 1

          setCurrentSecond(secondsPassed.current)
        }
      }, 1000);
    }

    return () => void clearInterval(timer)
  }, [timerStarted])

  return (
    <div>
      <div>
        <button onClick={() => setTimerStarted(!timerStarted)}>
          {timerStarted ? Stop : Start}
        </button>
        <p>{currentSecond}</p>
      </div>

    </div>
  );
}

export default Play;

Pourquoi avez-vous besoin d'un ref et de l'état? Si vous n'aviez que l'état, la méthode de nettoyage de l'effet s'exécuterait chaque fois que vous mettriez à jour votre état. Par conséquent, vous ne voulez pas que votre état influence votre effet. Vous pouvez y parvenir en utilisant le ref pour compter les secondes. Les modifications apportées à la référence n'exécuteront pas l'effet ou ne le nettoieront pas.

Cependant, vous avez également besoin de l'état car vous voulez que votre composant soit à nouveau rendu une fois que votre condition est remplie. Mais comme les méthodes de mise à jour pour l'état (c'est-à-dire setCurrentSecond ) sont constantes, elles n'influencent pas non plus l'effet.

Dernier point mais non le moindre, j'ai découplé la configuration de l'intervalle de votre logique de comptage. J'ai fait cela avec un état supplémentaire qui bascule entre true et false . Ainsi, lorsque vous cliquez sur votre bouton, l'état passe à true , l'effet est exécuté et tout est configuré. Si vos composants démontent, ou si vous arrêtez le chronomètre, ou si le prop secondsPerRep change, l'ancien intervalle est effacé et un nouveau est configuré.

/ p>


1 commentaires

Je suppose que l'utilisation de ref pour le résoudre est un peu exagéré



0
votes

Essayez ça. Le problème était que vous n'utilisez pas l'état qui est reçu par la fonction setCurrentSecond et la fonction setInterval ne voit pas l'état changer.

const Play = props => {
  const [currentSecond, setCurrentSecond] = useState(1);
  const [timer, setTimer] = useState();
  const onClick = () => {
    setTimer(setInterval(() => {
      setCurrentSecond((state) => {
        if (state < props.secondsPerRep) {
          return state + 1;
        }
        return state;
      });
    }, 1000));
  }

  return (
    <div>
      <div>
        <button onClick={onClick} disabled={timer}>Start</button>
        <p>{currentSecond}</p>
      </div>

    </div>
  );
}


0 commentaires

1
votes

https://overreacted.io/making-setinterval-declarative- with-react-hooks / est un excellent article à consulter et à en savoir plus sur ce qui se passe. Le hook useState de React ne joue pas bien avec setInterval car il n'obtient que la valeur du hook dans le premier rendu, puis continue de réutiliser cette valeur plutôt que la valeur mise à jour des futurs rendus.

Dans ce post, Dan Abramov donne un exemple de hook personnalisé pour faire fonctionner les intervalles dans React que vous pourriez utiliser. Cela rendrait votre code plus semblable à ceci. Notez que nous devons changer la façon dont nous déclenchons le minuteur pour qu'il démarre avec une autre variable d'état.

const Play = props => {
  const [currentSecond, setCurrentSecond] = React.useState(1);
  const [isRunning, setIsRunning] = React.useState(false);
  useInterval(() => {
    if (currentSecond < props.secondsPerRep) {
      setCurrentSecond(currentSecond + 1);
    }
  }, isRunning ? 1000 : null);

  return (
    <div>
      <div>
        <button onClick={() => setIsRunning(true)}>Start</button>
        <p>{currentSecond}</p>
      </div>
    </div>
  );
}

J'ai continué et j'ai mis un exemple de codepen ensemble pour votre cas d'utilisation si vous voulez jouer avec et voir comment cela fonctionne.

https://codepen.io / BastionTheDev / pen / XWbvboX


0 commentaires