Jason T.
Jason T.

Reputation: 113

glitch from React hooks?

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: screenshot

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

Answers (1)

Drew Reese
Drew Reese

Reputation: 202706

Issues

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.

Suggested Solution

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.

Demo

Edit glitch-from-react-hooks

Upvotes: 1

Related Questions