Home > Mobile >  React's useEffect unstable delay
React's useEffect unstable delay

Time:05-03

I'm working on a Pomodoro clock. To build the countdowns I'm using useEffect and setTimeout. Everything seemed to be fine until I realized there's a 30ms to 50ms delay between every second of the clock. How would I set it to pricesily update the clock at every 1000ms?

I'm using useState to handle the clock's time and controls to stop, pause and reset. They're all working properly. It's just the second's timing that is delaying more that it should.

function App() {
  const [workTarget, setWorkTarget] = useState(25 * 60);
  const [breakTarget, setBreakTarget] = useState(5 * 60);
  const [time, setTime] = useState(workTarget); //time in seconds
  const [counting, setCounting] = useState(false);
  const [working, setWorking] = useState(true);
  const [clockTarget, setClockTarget] = useState(workTarget);
  const [combo, setCombo] = useState(0);
  const [config, setConfig] = useState(false);
  const [playWork] = useSound(workSfx);
  const [playBreak] = useSound(breakSfx);
  const [playPause] = useSound(pauseSfx);
  let tick = 1000;
  let timeout;
  let timenow = Date.now();

// Handle pause and stop of countdown
  useEffect(() => {
    if (time > 0 && counting === true) {
      timeout = setTimeout(() => {
        setTime(time - 1);
        console.log(timenow);
      }, tick);
    } else if (time === 0 && counting === true) {
      setWorking(!working);
      if (working === true) {
        playBreak();
        setTime(breakTarget);
        setClockTarget(breakTarget);
      } else {
        playWork();
        setCombo(combo   1);
        setTime(workTarget);
        setClockTarget(workTarget);
      }
    }
    if (!counting || config) {
      clearTimeout(timeout);
    }
  });
}

export default App;

This is not the complete code. I cut off other components for buttons and stuff that don't relate to this.

CodePudding user response:

I may have an idea.

Currently your useEffect hook is set up to run on every render which means every time you change the Time you render the page and rerun the useEffect hook, however you seem to have quite a bit of logic inside the hook, plus react may not render consistently which means that there may be some inconsistencies with your timing.

I suggest using setInterval instead and running the useEffect hook only once at the start to set up the interval using useEffect(() => {...}, []) (notice [] as the second argument)

For the rest of your logic you can always create another useEffect hook that updates when the components change.

with all that in mind your timing logic should look something like this:

useEffect(() => {
    interval = setInterval(() => {
        setTime(time-1);
        console.log(timenow);
    }, tick); //runs once every tick
    return(() => clearInterval(interval)); //Once the component unmounts run some code to clear the interval
}, [])

Now in another useEffect hook you can watch for changes to time and update the rest of your app accordingly.

This approach should be much more reliable because it doesn't depend on react to update the time.

I hope this helps!

CodePudding user response:

We can't ensure its EXACTLY 1000 ms. Behind the scenes, react state updates use setTimeout, and to change the timer, you need to use either setTimeout or setInterval. setTimeout and setInterval only ensure that the code inside will not run for a delay period, and then executes when the main thread is not busy.

Therefore, it's impossible to ensure that every update is EXACTLY 1000ms. There will usually be 30-50ms delay.

However, that doesn't mean your timer will be inaccurate or unreliable. It just depends how you initialize it.

Below is how I would improve the code you provided, as right now clearing the timeout adds some extra overhead, and batching the state updates would lead to improved performance in this case.

 let tick = 1000;
  //countdown modifying behavior every 1000ms. Active clock
  useEffect(() => {
        if (time > 0 && counting === true) {
          setTimeout(() => {
            setTime((state) => state - 1);
            console.log(timenow);
          }, tick);
        } 
  }, [time, counting]);
  //pausing, stopping and resuming
  useEffect(() => {
     if (time === 0 && counting === true) {
      setWorking(!working);
      if (working === true) {
        playBreak();
        unstable_batchedUpdates(() =>{
           setTime(breakTarget);
           setClockTarget(breakTarget);
        }
      } else {
        playWork();
        //update all at once
        unstable_batchedUpdates(() =>{
           setCombo(combo   1);
           setTime(workTarget);
           setClockTarget(workTarget);
        }
      }
    }
  }, [time, counting]);

Further improvements to keep the clock in sync

  1. On component mount, grab the current time for start time, and calculate the end time and store both
  2. At every setTimeout in your useEffect, using your stored start time and current time, calculate how much time there is left and set state to that.
      //countdown time modifying behavior every 1000ms
      const current = new Date()
      const [start, setStart] = useState(current) 
      //this example uses a 25 minute timer as an example
      const [end, setEnd] = useState(new Date(start.getTime()   25 * 60000))
      const tick = 1000
      useEffect(() => {
            if (time > 0 && counting === true) {
              setTimeout(() => {
                const current = new Date()
                const diffTime = current.getDate() - end.getDate()
                const timeRemainingInMins = Math.ceil(diffTime / (1000*60)); 

                setTime(timeRemainingInMins);
              }, tick);
            } 
      }, [time, counting]);

CodePudding user response:

#The_solution

const component =() => {
const [val, setval]= useState(globalState.get());

useEffect(() => {
setVal(globalState.get());
globalState.subscribe(newVal=> setVal(newVal));
});
return <span>{val}</span>
}
  • Related