Search code examples
reactjsreact-hookssetintervaluse-effectuse-state

React setInterval in UseEffect not updating correctly in background tab


I am trying to code a (pomodoro) timer, and I keep running into this issue where if the code is running in a background tab for a period of time, about 5-10 minutes + (on chrome at least), it lags behind and ends up taking longer than it should to complete the timer. I have changed the interval to be 1000 ms, and tried to use refs instead of state.

Code is available on github

Demo at gh-pages

import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { ReactDOM } from 'react-dom';

const Timer = ({ time, addTomato }) => {
    const [ timeRemaining, setTimeRemaining ] = useState(time * 60);
    const [ isPaused, setIsPaused ] = useState(true);
    const timeRemainingRef = useRef(timeRemaining);
    const isPausedRef = useRef(isPaused);

    let navigate = useNavigate();
    let minutes = Math.floor(timeRemaining / 60).toLocaleString('en-US', {
        minimumIntegerDigits: 2,
        useGrouping: false
    });
    let seconds = Math.floor(timeRemaining % 60).toLocaleString('en-US', {
        minimumIntegerDigits: 2,
        useGrouping: false
    });

    function timer() {
        if (isPausedRef.current) return;
        if (timeRemainingRef.current === 0) {
            addTomato();
            navigate("/trackodoro/break/");
            return
        }
        timeRemainingRef.current --;
        document.title = `Trackodoro ${Math.floor(timeRemainingRef.current / 60).toLocaleString('en-US', {
        minimumIntegerDigits: 2,
        useGrouping: false
    })}:${Math.floor(timeRemainingRef.current % 60).toLocaleString('en-US', {
        minimumIntegerDigits: 2,
        useGrouping: false
    })}`
        setTimeRemaining(timeRemainingRef.current)
    }

    useEffect( //maybe i dont wanty to use useeffect
        () => {
            const myTimer = setInterval(timer, 1000);
            return () => clearInterval(myTimer); //when I pause, it doesnt count down to next number
        },
        []
    );

    const pauseTimer = () => {
        isPausedRef.current = !isPausedRef.current
        setIsPaused(isPausedRef.current);
    };

    const resetTimer = () => {
        !isPausedRef.current && pauseTimer();
        timeRemainingRef.current = time * 60
        setTimeRemaining(timeRemainingRef.current);
    }

    return (
        <div>
            <h1>
                {minutes}:{seconds}
            </h1>
            <button className="btn btn-block" onClick={pauseTimer}>
                {isPaused && timeRemaining === time * 60 ? 'Start' : isPaused ? 'Resume' : 'Pause'}
            </button>
            <button className="btn btn-block" onClick={resetTimer}>
                Reset
            </button>
        </div>
    );
};

export default Timer;

Solution

  • instead of timeRemainingRef.current --, you should probably want to use a end datetime.

    Example (psuedo code) with date-fns.

    const expiredate = addMinutes(new Date(), 5) //expires in 5 minutes.
    

    At the place where you need timeRemainingRef.current --

    timeRemainingRef.current = differenceInSeconds(expiredate, new Date())
    

    timeRemainingRef.current will be the number of seconds left to the expiredate

    Using difference in datetime is probably more reliable than using a +1 counter.

    Reference for addMinutes - https://date-fns.org/v2.28.0/docs/addMinutes

    Reference for differenceInSeconds - https://date-fns.org/v2.28.0/docs/differenceInSeconds

    date-fns is a lightweight datetime utility.