Home > Back-end >  My timer in Reactjs is not working correctly with a delay
My timer in Reactjs is not working correctly with a delay

Time:05-07

My timer in Reactjs is not working correctly with a delay...

I noticed that over time, my timer starts to lag if I switch to another browser tab or window. Why is this happening and how to fix it?

import React, { Component } from 'react';

class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0
    }
  }

  componentDidMount() {
    this.timer = setInterval(() => {
      this.setState((prevState) => ({ counter: prevState.counter > 0 ? prevState.counter - 1 : 0 }));
    }, 1000);
  }

  componentDidUpdate() {
    var target_date = new Date();
    target_date.setHours(23,59,59);
    var current_date = new Date();
    current_date.getHours();
    var counter = (target_date.getTime() - current_date.getTime()) / 1000;
    if (this.state.counter === 0) {
      this.setState({ counter: counter });
    }
  }

  componentWillUnmount() {
    clearInterval(this.timer);
  }

  render() {
    const padTime = time => {
      return String(time).length === 1 ? `0${time}` : `${time}`;
    };

    const format = time => {
      const hours = Math.floor(time / (60 * 60));
      const minutes = Math.floor(time % (60 * 60) / 60);
      const seconds = time % 60;
      return `${padTime(hours)}:${padTime(minutes)}:${padTime(seconds)}`;
    };

    return (
  <div>{format(this.state.counter)}</div>
    );
  }
}

export default Test;

Please help me!

CodePudding user response:

setTimeout() and setInterval() don't always call the callback function on time. It can be called later if JavaScript is doing other stuff, and/or might be called less frequent if the tab is not focused.

The solution is fairly simple, instead of using a counter to keep track of the seconds past. You should store a reference point start. Then when rendering you can calculate counter by comparing start with the current time.

Here is an example using an interval of 1ms to better display the difference.

const { useState, useEffect } = React;

// Does NOT correctly keep track of time.
function TimerA() {
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    const intervalID = setInterval(() => setCounter(counter => counter   1), 1);
    return () => clearInterval(intervalID);
  }, []);

  return (
    <div>
      I will lag behind at some point.<br />
      Milliseconds passed: {counter}
    </div>
  );
}

// Does correctly keep track of time.
function TimerB() {
  const [start] = useState(Date.now());
  const [now, setNow] = useState(start);
  const counter = now - start;
  
  useEffect(() => {
    const intervalID = setInterval(() => setNow(Date.now()), 1);
    return () => clearInterval(intervalID);
  }, []);

  return (
    <div>
      I will always stay on time.<br />
      Milliseconds passed: {counter}
    </div>
  );
}

ReactDOM.render(
  <div>
    <TimerA />
    <hr />
    <TimerB />
  </div>,
  document.querySelector("#root")
);
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>

Don't use 1ms in production code. If the callback takes more time to execute than the interval (which it probably does in the above snippet). Then the callback queue will only grow until eventually something gives.

If you are using class components you don't have to store now in a state. Instead it could be a normal variable const now = Date.now() and you would use setInterval(() => this.forceUpdate(), ...). The only reason I store now in a state is because functional components don't have access to forceUpdate().


Now that you hopefully understand the issue and the solution, we can come back to the code of the question.

Since you are calculating the time until midnight there is no point in storing start. Instead you should calculate the next midnight and use that as reference point. Then calculated the time between now and midnight. Both should be done for each render.

<script type="text/babel">
// time constants
const MILLISECOND = 1, MILLISECONDS = MILLISECOND;
const SECOND = 1000*MILLISECONDS, SECONDS = SECOND;
const MINUTE = 60*SECONDS, MINUTES = MINUTE;
const HOUR = 60*MINUTES, HOURS = HOUR;
const DAY = 24*HOURS, DAYS = DAY;

function nextMidnight() {
  const midnight = new Date();
  midnight.setHours(0, 0, 0, 0);
  return midnight.valueOf()   (1*DAY);
}

function Timer({ ms }) {
  const hours = Math.floor(ms / (1*HOUR));
  const minutes = Math.floor(ms % (1*HOUR) / (1*MINUTE));
  const seconds = Math.floor(ms % (1*MINUTE) / (1*SECOND));

  const pad = (n) => n.toString().padStart(2, "0");
  return <>{pad(hours)}:{pad(minutes)}:{pad(seconds)}</>;
}

class Test extends React.Component {
  componentDidMount() {
    this.intervalID = setInterval(() => this.forceUpdate(), 1*SECOND);
  }

  componentWillUnmount() {
    clearInterval(this.intervalID);
  }
  
  render() {
    const msUntilMidnight = nextMidnight() - Date.now();
    
    return (
      <div>
        <Timer ms={msUntilMidnight} />
      </div>
    );
  }
}

ReactDOM.render(<Test />, document.querySelector("#root"));
</script>

<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>

CodePudding user response:

This is not related to react, it's the browser's behavior, browser would attempt to save computing power when not focused. Probably could see this one https://stackoverflow.com/a/6032591/649322

Not sure what you mean by "lag", but if you'd like to keep as precise as possible, you could replace setInterval with setTimeout and calculate the exact time of next tick.

For example:

  constructor(props) {
    super(props);

    this.timer = undefined;
    this.prevTime = 0;
  }

  componentDidMount() {
    const INTERVAL = 1000;
    function update() {
      console.log('do something');
  
      const now = Date.now();
      const delay = now - this.prevTime - INTERVAL;
      const next = INTERVAL - diff;
      this.setTimeout(update, next);
      this.prevTime = now;
      
      console.log('debug', Math.floor(now / 1000), delay, next);
    }
    update(); // fire the update
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  • Related