web-dev-qa-db-fra.com

réagir les crochets et setInterval

Existe-t-il une alternative à garder une "horloge" en arrière-plan pour implémenter auto-next (après quelques secondes) dans le carrousel en utilisant des crochets de réaction?

Le crochet de réaction personnalisé ci-dessous implémente un état pour un carrousel qui prend en charge les méthodes manuelles (suivant, précédent, réinitialiser) et automatiques (démarrer, arrêter) pour changer l'index actuel (actif) du carrousel.

const useCarousel = (items = []) => {
  const [current, setCurrent] = useState(
    items && items.length > 0 ? 0 : undefined
  );

  const [auto, setAuto] = useState(false);

  const next = () => setCurrent((current + 1) % items.length);
  const prev = () => setCurrent(current ? current - 1 : items.length - 1);
  const reset = () => setCurrent(0);
  const start = _ => setAuto(true);
  const stop = _ => setAuto(false);


useEffect(() => {
    const interval = setInterval(_ => {
      if (auto) {
        next();
      } else {
        // do nothing
      }
    }, 3000);
    return _ => clearInterval(interval);
  });

  return {
    current,
    next,
    prev,
    reset,
    start,
    stop
  };
};
2
marco alves

Parce que la valeur current va changer à chaque "intervalle" tant que devrait être en cours d'exécution, votre code va alors démarrer et arrêter un nouveau minuteur à chaque rendu. Vous pouvez voir ceci en action ici:

https://codesandbox.io/s/03xkkyj19w

Vous pouvez changer setInterval en setTimeout et vous obtiendrez exactement le même comportement. setTimeout n'est pas une horloge persistante, mais cela n'a pas d'importance, car ils sont tous deux nettoyés de toute façon.

Si vous ne voulez pas lancer de minuteur, mettez la condition avant setInterval et non à l'intérieur.

  useEffect(
    () => {
      let id;

      if (run) {
        id = setInterval(() => {
          setValue(value + 1)
        }, 1000);
      }

      return () => {
        if (id) {
          alert(id) // notice this runs on every render and is different every time
          clearInterval(id);
        }
      };
    }
  );
1
azium

Il existe des différences entre setInterval et setTimeout que vous ne souhaitez peut-être pas perdre en redémarrant toujours votre minuterie lorsque le composant effectue un nouveau rendu. Ce violon montre la différence de dérive entre les deux (et cela ne prend même pas en compte tous les calculs effectués par React, ce qui pourrait jeter un peu plus loin le tout).

En vous référant maintenant à votre réponse , Marco, l'utilisation de setInterval est totalement perdue, car les effets sans conditions disposent et sont réexécutés chaque fois que le composant effectue un nouveau rendu. Ainsi, dans votre premier exemple, l'utilisation de la dépendance current entraîne la suppression et la réexécution de cet effet chaque fois que la variable current change (chaque fois que l'intervalle est exécuté). Le second fait la même chose, mais chaque fois qu’un état change (ce qui provoque un re-rendu), un comportement inattendu peut survenir. La seule raison pour laquelle on travaille est parce que next() provoque un changement d'état.

Considérant le fait que vous n'êtes probablement pas concerné par le timing exact, il est préférable d'utiliser setTimeout de manière simple, en utilisant les variables current et auto comme dépendances. Donc, pour reformuler une partie de votre réponse, procédez comme suit:

useEffect(
  () => {
    if (!auto) return;
    const interval = setInterval(_ => {
      next();
    }, autoInterval);
    return _ => clearInterval(interval);
  },
  [auto, current]
);

Notez que je pense que vous pourriez remplacer la dépendance de current par next puisque cela représente plus directement ce que vous faites dans useEffect. Mais je ne suis pas sûr à 100% de la façon dont React diffère ces dépendances, donc je le laisse tel quel.

Généralement, pour ceux qui lisent simplement cette réponse et veulent un moyen de créer un simple minuteur, voici une version qui ne prend pas en compte le code d'origine du PO, ni leur besoin de pouvoir démarrer et arrêter le minuteur indépendamment:

const [counter, setCounter] = useState(0);
useEffect(
  () => {
    const id= setTimeout(() => {
      setCounter(counter + 1);
    }, 1000);
    return () => {
      clearTimer(id);
    };
  },
  [counter],
);

Cependant, vous vous demandez peut-être comment utiliser un intervalle plus exact, étant donné que setTimeout peut dériver plus que setInterval. Voici une méthode, encore une fois, générique sans utiliser le code de l'OP:

const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
  () => {
    const id = setInterval(() => {
      r.current.setCounter(r.current.counter + 1);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  },
  ['once'],
);

Que se passe-t-il ici? Eh bien, pour obtenir le rappel de setInterval afin de toujours faire référence à la version actuellement acceptable de setCounter, nous avons besoin d’un état mutable. Réagir nous donne ceci avec useRef. La fonction useRef renverra un objet ayant une propriété current. Nous pouvons ensuite définir cette propriété (ce qui se produira chaque fois que le composant effectuera un nouveau rendu) sur les versions actuelles de counter et setCounter.

Ensuite, pour éviter que l'intervalle ne soit supprimé à chaque rendu, nous ajoutons à useEffect une dépendance dont la garantie est de ne jamais changer. Dans mon cas, j'aime bien utiliser la chaîne "once" pour indiquer que je force cet effet à être configuré une seule fois. L'intervalle sera toujours supprimé lorsque le composant sera démonté.

Donc, en appliquant ce que nous savons à la question initiale du PO, vous pouvez utiliser setInterval pour un diaporama moins susceptible de dériver comme ceci:

// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...

const r = useRef(null);
r.current = { next };
useEffect(
  () => {
    if (!auto) return;
    const id = setInterval(() => {
      r.current.next();
    }, autoInterval);
    return () => {
      clearInterval(id);
    };
  },
  [auto],
);
1
Don

Jusqu'à présent, il semble que les deux solutions ci-dessous fonctionnent comme souhaité:

Création conditionnelle de la minuterie - cela nécessite que useEffect dépend à la fois de auto et de current pour fonctionner

useEffect(
    () => {
      if (!auto) return;
      const interval = setInterval(_ => {
        next();
      }, autoInterval);
      return _ => clearInterval(interval);
    },
    [auto, current]
  );

Exécution conditionnelle de la mise à jour vers l'état - elle ne nécessite pas de dépendances useEffect 

useEffect(() => {
    const interval = setInterval(_ => {
      if (auto) {
        next();
      } else {
        // do nothing
      }
    }, autoInterval);
    return _ => clearInterval(interval);
  });

Les deux solutions fonctionnent si nous remplaçons setInterval par setTimeout

0
marco alves

Vous pouvez utiliser le crochet useTimeout qui renvoie true après le nombre spécifié de millisecondes.

0
Vad