import { useState, useEffect } from "react"; import "./styles.css"; export default function App() { const [progress, setProgress] = useState(0); let progressTimer; function handleTime() { if (progress <= 100) { console.log("Progress: " + progress); setProgress((prevState) => (prevState += 10)); } else { console.log("greater"); clearInterval(progressTimer); } } function handlePlay() { console.log("Timer start"); progressTimer = setInterval(handleTime, 1000); } useEffect(() => { handlePlay(); }); return ( <div className="App"> <h1>Hello CodeSandbox</h1> {progress} <h2>Start editing to see some magic happen!</h2> </div> ); }
Resultado deseado: Vaya a 100, contando de 10 en 10 cada 1 segundo. Una vez que llegue a más de 100, apague el temporizador.
Resultado real: simplemente sigue subiendo y subiendo, más rápido que 10 cada 1 segundo.
useEffect
para iniciar un intervalo no tiene una matriz de dependencia, por lo que se inició un nuevo intervalo cada vez que se representó el componente. Esto es lo que condujo a los saltos cada vez más grandes.progressTimer
se vuelve a declarar en cada ciclo de procesamiento, por lo que no hay forma de borrarlo.progress
se cierra en el alcance de devolución de llamada cuando se pasa a la devolución de llamada setInterval
. Solo estás mirando el valor del estado inicial. En otras palabras, es un recinto rancio.prevState => (prevState += 10)
en la actualización del estado funcional en realidad muta el estado anterior. Deben evitarse todas las mutaciones de estado.useEffect
para que se ejecute una vez en el montaje del componente. Mueva la lógica handlePlay
a la devolución de llamada del efecto para que no haya dependencias externas al montar. No olvide devolver una función de limpieza para borrar los intervalos de ejecución cuando se desmonte el componente.progressTimer
como una referencia React para que sea una referencia estable.useEffect
para verificar cuándo el valor progress
actual llega a 100.prevState => prevState + 10
como el siguiente valor de estado.Código
function App() { const [progress, setProgress] = useState(0); const progressTimer = useRef(); function handleTime() { setProgress((prevState) => prevState + 10); } useEffect(() => { console.log("Progress: " + progress); if (progress >= 100) clearInterval(progressTimer.current); }, [progress]); useEffect(() => { console.log("Timer start"); progressTimer.current = setInterval(handleTime, 1000); return () => clearInterval(progressTimer.current); }, []); return ( <div className="App"> <h1>Hello CodeSandbox</h1> {progress} <h2>Start editing to see some magic happen!</h2> </div> ); }
setInterval
con React, cuando se llama en el primer procesamiento, dará como resultado que la devolución de llamada del intervalo tenga un cierre obsoleto de las variables con estado después del primer procesamiento.
En su lugar, usaría setTimeout
, de modo que cada vez que se ejecute la devolución de llamada, tendrá el alcance del estado más actualizado.
const { useState, useEffect } = React; function App() { const [progress, setProgress] = useState(0); function handleTime() { if (progress <= 100) { console.log("Progress: " + progress); setProgress((prevState) => (prevState += 10)); } else { console.log("greater"); } } useEffect(() => { const timerId = setTimeout(handleTime, 1000); return () => clearTimeout(timerId); }); return ( <div className="App"> <h1>Hello CodeSandbox</h1> {progress} <h2>Start editing to see some magic happen!</h2> </div> ); } ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <div class='react'></div>