0
votes

setInterval dans React - comportement inattendu

J'ai écrit un simple chronomètre dans React.
Afin de rendre un nouveau nombre toutes les secondes, j'ai utilisé setInterval:

i is:  0
in incrementNumber, i is:  1
i is:  0
in incrementNumber, i is:  2
i is:  0
in incrementNumber, i is:  3
i is:  0
in incrementNumber, i is:  4
i is:  0
in incrementNumber, i is:  5
i is:  0

Code Sandbox

La question est: pourquoi ce chronomètre fonctionne-t-il comme prévu?
Puisque chaque seconde je re-rend le composant, la ligne let i = 0 est exécutée à nouveau, donc je m'attends à ce que la valeur renvoyée par incrementNumber sera toujours 1 (0 + 1)
Mais à la place, la valeur renvoyée est incrémentée: 1,2,3,4,5.
Voici la sortie de la console:

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

const MAX_TIMER = 5;

export default function StopWatch() {
  const [timer, setTimer] = useState(0);
  const tickIntervalId = useRef(null);

  const tickTimer = (getTimerTimeFn) => {
    const number = getTimerTimeFn();

    setTimer(number);

    if(number === MAX_TIMER) {
      clearInterval(tickIntervalId.current);
    }
  }

  let i = 0;
  console.log('i is: ', i) // i value is 0 every render
  const incrementNumber = () => {
    i += 1;
    console.log('in incrementNumber, i is: ', i); // i value is incrementing every render: 1, 2, 3, 4, 5. how?

    return i;
  }

  useEffect(() => {
    tickIntervalId.current = setInterval(tickTimer, 1000, incrementNumber);
  }, [])

  return (
    <div>
      {timer}
    </div>
  );
}


5 commentaires

Déplacez les déclarations i et incrementNumber vers la portée globale.


Le code fonctionne, comme vous pouvez le voir dans le bac à sable de code que j'ai joint. La question n'est pas de savoir comment le faire fonctionner.


@Indiquez-vous la variable i en dehors de la portée de la fonction, donc à chaque appel, elle s'incrémente. Si vous déclariez i dans la fonction incrementNumber , il se réinitialiserait toujours à zéro. Est-ce votre question? i est effectivement membre de StopWatch ()


@ DanielB.Chapman Ce n'est pas la question, la question est de savoir comment est-il possible que i continue d'augmenter à l'intérieur de la fonction incrementNumber , même si i non stocké dans l'état, et vous pouvez même voir dans la sortie de la console que i est mis à 0 à chaque rendu.


@Ido C'est une question de portée lexicale, c'est pourquoi déplacer i et incrementNumber vers la portée globale le casse.


4 Réponses :


2
votes

Vous devez connaître l'environnement lexical et le contexte d'exécution et le fonctionnement de l'oscilloscope

Déjà discuté ici exemple presque similaire
https://stackoverflow.com/a/19123476


0 commentaires

1
votes

Je pense que c'est une question de portée, si vous donnez console.log (i) dans useEffect, nous pouvons voir qu'il ne se connecte qu'au premier rendu. Comme si le let i à l'intérieur de useEffect était différent de celui du composant. Même avec le nouveau rendu, l'état dans useEffect demeure.

  useEffect(() => {
    console.log("i in effect: ", i)
    tickIntervalId.current = setInterval(tickTimer, 1000, incrementNumber);
  }, []);


1 commentaires

Merci d'avoir répondu. Cependant, la raison pour laquelle il n'est imprimé qu'une seule fois, c'est parce que cet useEffect ne s'exécute qu'une seule fois lorsque le composant se monte (à cause du tableau vide que j'ai fourni, c'est comme ComponentDidMount)



2
votes

Voici un petit exemple pour illustrer le problème de la portée lexicale.

function myFn() {
  
  let i = 0; // `i` is local to myFn() (has local scope)
  console.log(`myFn i == ${i}`);
  
  return (caller) => {
    i++;
    console.log(`${caller} : i == ${i}`);
  }
}

let a = myFn(); // 'myFn i == 0'
a('a'); // 'a : i == 1'

let b = myFn(); // 'myFn i == 0'
b('b'); // 'b : i == 1'
b('b'); // 'b : i == 2'
b('b'); // 'b : i == 3'

a('a'); // 'a : i == 2'


0 commentaires

2
votes

@Ido

Voici un lien vers quelque chose qui a plus de sens: j'utilise un objet pour que vous puissiez voir où la variable a été stockée. J'ai également voté pour toutes les autres réponses car elles sont correctes. C'est un problème de portée, pas un problème de réaction.

https: // codesandbox .io / s / material-demo-wj0pm

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

const MAX_TIMER = 5;

let renderCount = 0;
export default function StopWatch() {
  const [timer, setTimer] = useState({});
  const tickIntervalId = useRef(null);

  const tickTimer = getTimerTimeFn => {
    const number = getTimerTimeFn();

    setTimer({...number});

    if (number === MAX_TIMER) {
      clearInterval(tickIntervalId.current);
    }
  };

  var iObject = {
    i: 0,
    inRender: renderCount,
  };
  renderCount++;
  console.log("i is: ", JSON.stringify(iObject) ); // i value is 0 every render
  const incrementNumber = () => {
    iObject.i += 1;
    console.log("in incrementNumber, i is: ", JSON.stringify(iObject)); // i value is incrementing every render: 1, 2, 3, 4, 5. how?
    console.log("------------------------");
    return iObject; //Not a copy
  };

  useEffect(() => {
    tickIntervalId.current = setInterval(tickTimer, 1000, incrementNumber);
  }, []);

  return <div>{JSON.stringify(timer)}</div>;
}

entrez la description de l'image ici


0 commentaires