Saltar al contenido

El estado no se actualiza cuando se usa el enlace de estado React dentro de setInterval

Posterior a consultar expertos en la materia, programadores de varias ramas y profesores dimos con la respuesta al dilema y la plasmamos en esta publicación.

Solución:

La razón es porque la devolución de llamada pasó a setIntervalEl cierre de solo accede a la time variable en el primer render, no tiene acceso a la nueva time valor en el render posterior porque el useEffect() no se invoca la segunda vez.

time siempre tiene el valor de 0 dentro del setInterval llamar de vuelta.

Como el setState con el que está familiarizado, los ganchos de estado tienen dos formas: una donde toma el estado actualizado y la forma de devolución de llamada en la que se pasa el estado actual. Debe usar la segunda forma y leer el último valor de estado dentro del setState devolución de llamada para asegurarse de que tiene el valor de estado más reciente antes de incrementarlo.

Bonificación: enfoques alternativos

Dan Abramov, profundiza en el tema del uso setInterval con ganchos en su publicación de blog y proporciona formas alternativas de solucionar este problema. ¡Recomiendo leerlo!

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 (
    
Seconds: time
); ReactDOM.render(, document.querySelector('#app'));



useEffect La función se evalúa solo una vez en el montaje del componente cuando se proporciona una lista de entrada vacía.

una alternativa a setInterval es establecer un nuevo intervalo con setTimeout cada vez que se actualiza el estado:

  const [time, setTime] = React.useState(0);
  React.useEffect(() => 
    const timer = setTimeout(() => 
      setTime(time + 1);
    , 1000);
    return () => 
      clearTimeout(timer);
    ;
  , [time]);

El impacto en el rendimiento de setTimeout es insignificante y generalmente puede ser ignorado. A menos que el componente sea sensible al tiempo hasta el punto en que los tiempos de espera recién establecidos causen efectos no deseados, ambos setInterval y setTimeout Los enfoques son aceptables.

Como otros han señalado, el problema es que useState sólo se llama una vez (como deps = []) para configurar el intervalo:

React.useEffect(() => 
    const timer = window.setInterval(() => 
        setTime(time + 1);
    , 1000);

    return () => window.clearInterval(timer);
, []);

Entonces, cada vez setInterval garrapatas, en realidad llamará setTime(time + 1)pero time siempre mantendrá el valor que tenía inicialmente cuando el setInterval se definió la devolución de llamada (cierre).

Puede utilizar la forma alternativa de useStatesetter y proporciona una devolución de llamada en lugar del valor real que desea establecer (al igual que con setState):

setTime(prevTime => prevTime + 1);

Pero te animo a que crees tu propio useInterval gancho para que pueda SECAR y simplificar su código usando setInterval declarativamente, como sugiere Dan Abramov aquí en Making setInterval Declarative with React Hooks:

function useInterval(callback, delay) 
  const intervalRef = React.useRef();
  const callbackRef = React.useRef(callback);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setInterval ticks again, it
  // will still call your old callback.
  //
  // If you add `callback` to useEffect's deps, it will work fine but the
  // interval will be reset.

  React.useEffect(() => 
    callbackRef.current = callback;
  , [callback]);

  // Set up the interval:

  React.useEffect(() => 
    if (typeof delay === 'number') 
      intervalRef.current = window.setInterval(() => callbackRef.current(), delay);

      // Clear interval if the components is unmounted or the delay changes:
      return () => window.clearInterval(intervalRef.current);
    
  , [delay]);
  
  // Returns a ref to the interval ID in case you want to clear it manually:
  return intervalRef;



const Clock = () => 
  const [time, setTime] = React.useState(0);
  const [isPaused, setPaused] = React.useState(false);
        
  const intervalRef = useInterval(() => 
    if (time < 10) 
      setTime(time + 1);
     else 
      window.clearInterval(intervalRef.current);
    
  , isPaused ? null : 1000);

  return (
    

    

time.toString().padStart(2, '0') /10 sec.

setInterval time === 10 ? 'stopped.' : 'running...'

); ReactDOM.render(, document.querySelector('#app'));
body,
button 
  font-family: monospace;


body, p 
  margin: 0;


p + p 
  margin-top: 8px;


#app 
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;


button 
  margin: 32px 0;
  padding: 8px;
  border: 2px solid black;
  background: transparent;
  cursor: pointer;
  border-radius: 2px;



Además de producir un código más simple y limpio, esto le permite pausar (y borrar) el intervalo automáticamente simplemente pasando delay = null y también devuelve la identificación del intervalo, en caso de que desee cancelarlo usted mismo manualmente (eso no está cubierto en las publicaciones de Dan).

De hecho, esto también podría mejorarse para que no reinicie el delay cuando no está en pausa, pero supongo que para la mayoría de los casos de uso esto es lo suficientemente bueno.

Si está buscando una respuesta similar para setTimeout en vez de setIntervalmira esto: https://stackoverflow.com/a/59274757/3723993.

También puede encontrar la versión declarativa de setTimeout y setInterval, useTimeout y useIntervalmás una costumbre useThrottledCallback gancho escrito en TypeScript en https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a.

Si te gustó nuestro trabajo, eres capaz de dejar un post acerca de qué le añadirías a esta crónica.

¡Haz clic para puntuar esta entrada!
(Votos: 2 Promedio: 4)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *