Yustina Yasin
Yustina Yasin

Reputation: 319

setInterval in react does not update the setState

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

Answers (2)

Drew Reese
Drew Reese

Reputation: 202595

Issue

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)

Solution

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.

Details

  1. The minute and seconds is considered derived state (from remainingTime) so it shouldn't also be stored in state, it is easily computed from state.
  2. Use a functional state update to update the remainingTime state from the previous state, not the state of the render cycle the callback was enqueued in.
  3. Use a React ref to hold an interval timer ref, so the interval can be cleared.
  4. Use an 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>
  );
}

Demo

enter image description here

Edit setinterval-in-react-does-not-update-the-setstate

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

Achal Jain
Achal Jain

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

Related Questions