El estado no se actualiza cuando se usa el enlace de estado de React dentro de setInterval
Estoy probando los nuevos React Hooks y tengo un componente de reloj con un time
valor que se supone que aumenta cada segundo. Sin embargo, el valor no aumenta más allá de uno.
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
La razón es que la devolución de llamada pasada al setInterval
cierre de solo accede a la time
variable en la primera representación, no tiene acceso al nuevo time
valor en la representación posterior porque useEffect()
no se invoca la segunda vez.
time
siempre tiene el valor de 0 dentro de la setInterval
devolución de llamada.
Como setState
ya conoce, los enlaces de estado tienen dos formas: una en la que 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 de la devolución de setState
llamada para asegúrese de tener el último valor de estado antes de incrementarlo.
Bonificación: enfoques alternativos
Dan Abramov profundiza en el tema sobre el uso
setInterval
de ganchos en su publicación de blog y ofrece formas alternativas de solucionar este problema. ¡Recomiendo mucho 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 (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
Como otros han señalado, el problema es que useState
solo se llama una vez (como deps = []
) para configurar el intervalo:
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => window.clearInterval(timer);
}, []);
Luego, cada vez setInterval
que marca, realmente llamará setTime(time + 1)
, pero time
siempre mantendrá el valor que tenía inicialmente cuando setInterval
se definió la devolución de llamada (cierre).
Puede usar la forma alternativa del useState
configurador y proporcionar 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 puedas SECAR y simplificar tu código usando setInterval
de forma declarativa , como sugiere Dan Abramov aquí en Cómo hacer que setInterval sea declarativo con ganchos de React :
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 (<React.Fragment>
<button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
{ isPaused ? 'RESUME ⏳' : 'PAUSE 🚧' }
</button>
<p>{ time.toString().padStart(2, '0') }/10 sec.</p>
<p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
</React.Fragment>);
}
ReactDOM.render(<Clock />, 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;
}
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="app"></div>
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 el ID del intervalo, en caso de que desee cancelarlo usted mismo manualmente (eso no se trata en las publicaciones de Dan).
En realidad, esto también podría mejorarse para que no se reinicie delay
cuando se reanude, pero supongo que para la mayoría de los casos de uso esto es suficiente.
Si está buscando una respuesta similar para setTimeout
en lugar de setInterval
, consulte esto: https://stackoverflow.com/a/59274757/3723993 .
También puede encontrar una versión declarativa de setTimeout
y setInterval
, useTimeout
y useInterval
, algunos enlaces adicionales escritos en TypeScript en https://www.npmjs.com/package/@swyg/corre .
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 setInterval
es establecer un nuevo intervalo 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 setTimeout
es insignificante y, en general, puede ignorarse. A menos que el componente sea sensible al tiempo hasta el punto en que los tiempos de espera recién establecidos causen efectos indeseables, ambos setInterval
enfoques setTimeout
son aceptables.