Reputation: 113
I am trying to build a countdown timer app with start/pause button using React hooks.
However, the interaction does not responds as expected. As the screenshot of console.log shows here:
In Event 2, when pause button gets clicked, props.setIsPaused(true)
should set isPaused
true, but console.log shows false
. However, the value of isPaused
in parent component gets updated to true
according to console.log.
In Event 3, when start button gets clicked, props.setIsPaused(false)
should set isPaused
false, but console.log shows true
. However, the value of isPaused
in parent component gets updated to false
according to console.log.
In this case, countdown timer does not start, I need to click start button for 2nd time to have it finally start the countdown.
I am totally confused by these actions out of my expectations.
Any ideas or thoughts what I missed or mistakes I have?
Thank you.
Here is the code of the child/parent component performing timer countdown:
// Child Component
function TimerPanel(props) {
const launch = React.useRef();
var secLeft = parseInt(props.timerNow.min) * 60 + parseInt(props.timerNow.sec)
const startCountDown = () => {
props.setIsPaused(false) // Once start button is clicked, set isPaused false
console.log('props.isPaused inside startCountDown func: ' + props.isPaused) // This should be false
launch.current = setInterval(decrement, 1000)
}
function decrement() {
if (secLeft > 0 && !props.isPaused) {
console.log('props.isPaused inside decrement func: '+ props.isPaused)
secLeft--;
var minCountDown = Math.floor(secLeft / 60)
var secCountDown = Math.floor((secLeft % 60) * 100) / 100
props.setTimerNow({
...props.timerNow,
min: minCountDown,
sec: secCountDown})
}
}
const pauseCountDown = () => {
props.setIsPaused(true) // Once pause button is clicked, set isPaused true
console.log('props.isPaused inside pauseCountDown func: '+ props.isPaused) // This should be true
clearInterval(launch.current)
}
......
}
// Parent Component
function App() {
const [breakLength, setBreakLength] = React.useState(5)
const [sessionLength, setSessionLength] = React.useState(25)
const [title, setTitle] = React.useState('Session')
const initialTimer = {
min: 25,
sec: 0
}
const [timerNow, setTimerNow] = React.useState(initialTimer)
const [isPaused, setIsPaused] = React.useState(false)
console.log(timerNow)
console.log('isPaused in App: ' + isPaused)
..........
}
Upvotes: 1
Views: 873
Reputation: 202706
I think what may've been tripping you up (I know it was me) was the split of logic between the app component and the timer panel component. All the state was declared in App
, but all the state was mutated/updated in children components. I think some state was also poorly named, which made working with it a little confusing.
I've directly answered your concerns about the reflected values of isPaused
being "delayed" in the comments, but I wanted to see how I could help.
Move all the logic to manipulate the timer state into the same component where the state is declared, pass functions to children to callback to "inform" the parent to do some "stuff".
In the following example I was able to greatly reduce the burden of TimerPanel
trying to compute the time to display and trying to update it and keep in sync with the paused state from the parent.
TimerPanel
Receives timer value, in seconds, and computes the displayed minutes and values since this is easily derived from state. Also receives callbacks to start/stop/reset the timer state.
function TimerPanel({
timerNow,
title,
startTimer,
pauseTimer,
resetTimer,
}) {
const minutes = Number(Math.floor(timerNow / 60))
const seconds = Number(Math.floor((timerNow % 60) * 100) / 100)
return (
<div className="timer-wrapper">
<div className="display-title">{title}</div>
<div className="display-digits">
{minutes.toString().padStart(2, "0")}:
{seconds.toString().padStart(2, "0")}
</div>
<div className="control-keysets">
<i className="fas fa-play-circle" onClick={startTimer}>
Play
</i>
<i className="fas fa-pause-circle" onClick={pauseTimer}>
Pause
</i>
<button className="reset" onClick={resetTimer}>
Reset
</button>
</div>
</div>
);
}
App
Simply timer state to be just time in seconds as this makes time management much simpler. Create handlers to start, pause, and reset the timer state. Use an useEffect
hook to run the effect of starting/stopping the time based on the component state. Do correct state logging in an useEffect
hook with appropriate dependency.
function App() {
const [breakLength, setBreakLength] = React.useState(5);
const [sessionLength, setSessionLength] = React.useState(25);
const [title, setTitle] = React.useState("Session");
const initialTimer = 60 * 25;
const [timerNow, setTimerNow] = React.useState(initialTimer);
const [isStarted, setIsStarted] = React.useState(false);
const timerRef = React.useRef();
const start = () => setIsStarted(true);
const pause = () => setIsStarted(false);
const reset = () => {
setIsStarted(false);
setTimerNow(initialTimer);
}
React.useEffect(() => {
console.log("isStarted in App: " + isStarted);
if (isStarted) {
timerRef.current = setInterval(() => setTimerNow(t => t - 1), 1000);
} else {
clearInterval(timerRef.current)
}
}, [isStarted]);
React.useEffect(() => {
console.log('timer', timerNow);
}, [timerNow])
return (
<div className="container">
<h1>Pomodoro Timer</h1>
<div className="group">
<div className="block">
<BreakLength
breakLength={breakLength}
setBreakLength={setBreakLength}
isPaused={!isStarted}
/>
</div>
<div className="block">
<SessionLength
sessionLength={sessionLength}
setSessionLength={setSessionLength}
timerNow={timerNow}
setTimerNow={setTimerNow}
isPaused={!isStarted}
/>
</div>
</div>
<div className="bottom-block">
<TimerPanel
title={title}
setSessionLength={setSessionLength}
timerNow={timerNow}
startTimer={start}
pauseTimer={pause}
resetTimer={reset}
/>
</div>
</div>
);
}
I left the BreakLength
and SessionLength
components intact as they don't appear to be utilized much yet.
Upvotes: 1