The following code demonstrates a react functional component that has a single state variable named time
. It has a button click to start
which fires a function named updateTimer
. This meant to move the timer from 0
to 1
to 2
to 3
and so on.
function timer() {
const [time, updateTime] = useState(0);
function updateTimer() {
setInterval(() => {
updateTime(time 1)
},1000)
}
}
return (
<>
<span>{time} seconds</span>
<button onClick={updateTimer}>Click To Start</button>
</>
)
But what happens is that the timer stops after 1
. Apparently, the value of time
does not get updated. Could someone please explain this?
CodePudding user response:
To update the state depending on the previous state use:
updateTime((prevTime) => prevTime 1)
CodePudding user response:
Because updateTime
updates the value of time
in the next render of the component, the time
known to the setInterval
callback is not updated and remained the initial value 0
, and resets to 1
at every interval.
In a useEffect
run the setter updateTime
based on previous value to correct the counting, also cleanup and recreate the interval as needed when the counting starts or stops.
For a basic example, the following useEffect
starts an interval when updating
is set true
by the button
. The return
runs when
updating
changes or when the component unmounted for cleaning up the interval, before a new one can be recreated.
useEffect(() => {
if (!updating) return;
const count = setInterval(() => {
updateTime((prev) => prev 1);
}, 1000);
return () => {
clearInterval(count);
};
}, [updating]);
updating
is added to the dependencies array so that useEffect
listens to the changes of this state, and creates a new interval if it is true
.
The setter updateTime
does not change and it sets time
based on the previous value internally, so updateTime
and time
do not need to be included in the dependencies array.
Example in full snippet:
const { useState, useEffect, Fragment } = React;
function Timer() {
const [time, updateTime] = useState(0);
const [updating, setUpdating] = useState(false);
useEffect(() => {
if (!updating) return;
const count = setInterval(() => {
updateTime((prev) => prev 1);
}, 1000);
return () => {
clearInterval(count);
};
}, [updating]);
return (
<Fragment>
<p>{time} seconds</p>
<button onClick={() => setUpdating((prev) => !prev)}>{`Click To ${
updating ? "Stop" : "Start"
}`}</button>
</Fragment>
);
}
const App = () => {
return (
<div className="App">
<Timer />
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
.App {
font-size: x-large;
}
button {
padding: 9px;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>