Reputation: 319
I want to make a countdown timer (25 minutes). This is the code in App.js file
import './App.css';
import React, { useState } from 'react';
function App() {
const [remainingTime, setRemainingTime] = useState(1500);
const [seconds, setSeconds] = useState(0);
const [minute, setMinute] = useState(remainingTime/60);
function timer() {
setRemainingTime(remainingTime-1);
let newMinute = remainingTime/60;
let minuteArray = [...newMinute.toString()];
setMinute(parseInt(minuteArray.slice(0,2).join("")));
setSeconds(remainingTime%60);
console.log(minute);
console.log(seconds);
}
return (
<div className="App">
<div className="pomodoro">
<div className="timer">{minute}:{seconds}</div>
<div className="button-container">
<button className="start" onClick={() => setInterval(timer, 1000)}>start</button>
</div>
</div>
</div>
);
}
export default App;
The interval does not update the state value. The minute value always 25 and the seconds value always 0. When I don't use setInterval and just use the timer function like this
<button className="start" onClick={timer}>start</button>
every time I click the start button, the value changes. Any idea? I know I should use clearInterval too but I don't know where to put it. Should I create a new function which contain setInterval and clearInterval?
Upvotes: 2
Views: 1189
Reputation: 202595
The main issue is stale enclosures of state in the timer
callback.
Within the interval callback, the standard form of setting a new value doesn't work because the callback keeps using the same (non-updated) state value each time it runs:
—this doesn't work: setValue(value + 1)
Use the alternate form of setting new value:
—this will work: setValue((value) => value + 1)
This form allows the callback to obtain a fresh value with the most recent state every time it runs.
minute
and seconds
is considered derived state (from remainingTime
) so it shouldn't also be stored in state, it is easily computed from state.remainingTime
state from the previous state, not the state of the render cycle the callback was enqueued in.useEffect
hook to return a clean up function to clear any running intervals when the component unmounts.Code:
function App() {
const timerRef = React.useRef();
React.useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
const [remainingTime, setRemainingTime] = React.useState(1500);
function timer() {
setRemainingTime((remainingTime) => remainingTime - 1);
}
const startTimer = () => {
clearInterval(timerRef.current); // clear any running interval
setRemainingTime(1500); // reset state back to 25 minutes
timerRef.current = setInterval(timer, 1000); // start/restart interval
};
const minute = String(Math.floor(remainingTime / 60)).padStart(2, 0);
const seconds = String(remainingTime % 60).padStart(2, 0);
return (
<div className="App">
<div className="pomodoro">
<div className="timer">
{minute}:{seconds}
</div>
<div className="button-container">
<button className="start" onClick={startTimer}>
start
</button>
</div>
</div>
</div>
);
}
Since this will likely be a follow-up question, use another useEffect
to "listen" to the remainingTime
state and when the time hits 0, clear the interval, reset remainingTime
back 1500 (for display), and show any alarm or alert or whatever for the pomodoro break/schedule.
Upvotes: 4
Reputation: 379
On every rerender, timer is set as 1500. Use closure here, activated on click.
function timer() {
let t = remainingTime;
setInterval(() => {
t -= 1;
setRemainingTime(t);
let newMinute = t / 60;
let minuteArray = [...newMinute.toString()];
setMinute(parseInt(minuteArray.slice(0, 2).join("")));
setSeconds(t % 60);
console.log(minute);
console.log(seconds);
}, 1000); }
Upvotes: 1