I'm trying to create a stopwatch in a React app. Inside a component (well, currently inside App()
created with vite), I have the following bits:
const [elapsedTime, setElapsedTime] = useState(0)
// this was just timer: number | undefined, which I've changed to this,
// but it didn't help, keep reading
let timer: { id?: number } = {
id: undefined,
}
const tickSecond = () => setElapsedTime(prevElapsedTime => prevElapsedTime + 1)
const resumeClock = () => timer.id = window.setInterval(tickSecond, 1000)
const pauseClock = () => {
window.clearInterval(timer.id)
timer.id = undefined
}
const toggleClockRun = () => {
console.log(`toggleClockRun: timer is`,timer)
timer.id ? pauseClock() : resumeClock()
}
const startClock = () => {
setElapsedTime(0)
resumeClock()
console.log(`startClock: timer is`,timer)
}
and in JSX:
<button onClick={startClock}>start</button>
<button onClick={toggleClockRun}>pause</button>
The expected behavior would be:
start
starts the stopwatch;pause
pauses it (well, toggles pause, but it's not important in this context).While startClock
reports startClock: timer is {id: 35}
(evaluated at the point of logging), toggleClockRun
unexpectedly reports startClock: timer is {id: undefined}
. How comes? Both when storing the timer id inside timer
as a number (timer = window.setInterval(..)
) or as the id
property of {id}
, inside toggleClockRun
although there's no other assigning of its value. Why timer.id
becomes undefined
?
PS For debugging, I've also tried to add {timer.id}
to JSX, and it never shown a non-empty value (but that's probably because editing the value didn't cause re-rendering).
The timer reference needs to be persisted between renders. The useRef
hook is used to store values that do not cause a re-render when updated.
const {useReducer, useRef} = React;
function reducer(currentState, newState) {
return {...currentState, ...newState};
}
function Stopwatch() {
const [{running, lapsedTime}, setState] = useReducer(reducer, {
running: false,
lapsedTime: 0,
});
const timerRef = useRef(null);
function toggleClockRun() {
if (running) {
clearInterval(timerRef.current);
} else {
const startTime = Date.now() - lapsedTime;
timerRef.current = setInterval(() => {
setState({lapsedTime: Date.now() - startTime});
}, 0)
}
setState({running: !running});
}
function resetButtonClick() {
clearInterval(timerRef.current);
setState({lapsedTime: 0, running: false});
}
return (
<div style={{textAlign: 'center'}}>
<label
style={{
display: 'block',
}}
>
{lapsedTime}
</label>
<button onClick={toggleClockRun}>
{running ? 'Stop' : 'Start'}
</button>
<button onClick={resetButtonClick}>
Reset
</button>
</div>
)
}
ReactDOM.render(<Stopwatch />, document.getElementById("app"));