I'm practicing react hooks and I'm creating a very simple stopwatch app. Currently, my code is doing exactly what I want it to do but I do not understand why it works. When I hit start, the setTimeouts run and constantly update the time state. When I hit stop, it clears the timeout. Why does it clear the timeout when I do not explicitly tell it to. Also, based on the react docs, the return in useEffect will only run when the component unmounts. However, I threw console.logs inside and saw that it runs the returned callback every time useEffect is called. Finally, I removed the returned callback and saw that it doesn't actually clear the timeout when I hit stop. Can someone help me dissect this?
import React, {useState, useEffect} from 'react';
function Stopwatch(){
const [time, setTime] = useState(0);
const [start, setStart] = useState(false);
useEffect(() => {
let timeout;
if (start) {
timeout = setTimeout(() => {setTime(currTime => currTime + 1);}, 1000);
}
return () => {
clearTimeout(timeout);
}
});
return(
<>
<div>{time}</div>
<button onClick={() => setStart(currStart => !currStart)}>{start ? "Stop" : "Start"}</button>
</>
)
}
export default Stopwatch
Why does it clear the timeout when I do not explicitly tell it to?
In your implementation useEffect runs after every re-render because you didn't specify the dependencies array, so if you start the timer and then in the middle press stop the clean up function is going to run and the last timeout will be cleared
It goes like this,
The component mounts -> useEffect callback fires and returns a function -> when the component re-renders, the returned function is executed and the cycle goes back to running the useEffect callback.
What you probably read in the docs had an empty dependencies array which is the second argument of useEffect
useEffect(() => {
console.log('will only run when the component mounts for the first time')
return () => {
console.log('will only run when the component unmounts')
}
}, []) // nothing inside the dependencies array, run this once
A better implementation of your component will be like this
function Stopwatch(){
const [time, setTime] = useState(0)
const [start, setStart] = useState(false)
useEffect(() => {
// when start is false there is no reason to set up a timer or return a
// cleanup function so lets just exit early
if (!start) return
// start is true, set up the interval
const intervalId = setInterval(() => setTime(prevTime => prevTime + 1), 1000)
// return a cleanup function that will run only when start changes
// to false
return () => clearInterval(intervalId)
}, [start]) // run this effect only when start changes
const toggleStart = () => setStart(prevStart => !prevStart)
return(
<>
<div>{time}</div>
<button onClick={toggleStart}>{start ? "Stop" : "Start"}</button>
</>
)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With