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 setInterval
El 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 useState
setter 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 setInterval
mira esto: https://stackoverflow.com/a/59274757/3723993.
También puede encontrar la versión declarativa de setTimeout
y setInterval
, useTimeout
y useInterval
má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.