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>
);
}
Desired outcome: Go to 100, counting by 10 every 1 second. Once you reach over 100, turn off the timer.
Actual outcome: It just keeps going up and up, faster than 10 every 1 second.
useEffect
to start an interval has no dependency array so a new interval was started each time the component rendered. This is what led to the bigger and bigger jumps.progressTimer
is redeclared each render cycle so there's no way to clear it.progress
state is closed over in callback scope when passed to the setInterval
callback. You're only ever looking at the initial state value. In other words, it's a stale enclosure.prevState => (prevState += 10)
in the functional state update actually mutates the previous state. All state mutations should be avoided.useEffect
so it runs once on component mount. Move the handlePlay
logic into the effect callback so there are no external dependencies when mounting. Don't forget to return a cleanup function to clear any running intervals when the component unmounts.progressTimer
as a React ref so it's a stable reference.useEffect
hook to check when the current progress
value reaches 100.prevState => prevState + 10
as the next state value.Code
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
with React, when called on the first render, will result in the interval callback having a stale closure of the stateful variable(s) after the first render.
I'd use setTimeout
instead, so that whenever the callback runs, it'll have scope of the most up-to-date state.
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>