Search code examples
javascriptreactjsmaterial-uiuse-effect

Behaviour of a function in useEffect when different browser tab is active


I have a weird issue. I've created a function that aims to reset the Linearprogress element after 60 seconds.


useEffect(() => {

    const interval2 = setInterval(() => {

        var internal = timer

        if( internal < 100 ) {internal = (internal - (1.695 * -1 )) } else {internal = internal - 100}

       setTimer(internal)
       
     
    

    }, 1000)
    return () => clearInterval(interval2)
  }, [timer])

Then, I have a render of linear progress element like this :


return (
    <div>
  
        <LinearProgress
         color ="secondary"
          value={timer}
          variant="determinate"
        />
      
    </div>
  );

Now the weird part : when Im looking at my app all looks normal, after 60 seconds the bar resets to start and that repeats on. However, when I change the active tab in the browser just after resetting and come back in 55 seconds ( the bar should be near the end ) - the bar is in the middle.

It looks like the useeffect doesnt re-execute the function as often as it should when the tab with the app is not active.

What am I missing here.

CODE SANDBOX ( issue replicated there) : https://codesandbox.io/s/young-brook-mttpz?file=/src/App.js:205-206

Thanks


Solution

  • You have a memory leak because of your setInterval. Each 1000ms, it will rerun but at the same time, your useEffect is also trigger by setTimer(internal);. So you have more and more setInterval running.

    One solution would be to add a clearInterval(interval2); before updating your Timer.

    But conceptually it's not perfect because we are using an interval as a timeout, so you can just replace your setInterval by a setTimeout and in the return clearInterval by clearTimeout without modifying anything else.

    Here is a working version of your code with that modification and the sandbox:

    import React from "react";
    import PropTypes from "prop-types";
    import { makeStyles } from "@material-ui/styles";
    import { LinearProgress } from "@material-ui/core";
    import { useEffect } from "react";
    
    const TotalProfit = (props) => {
      const [timer, setTimer] = React.useState(0);
    
      useEffect(() => {
        const interval2 = setTimeout(() => {
          var internal = timer;
    
          if (internal < 100) {
            internal = internal - 1.695 * -1;
          } else {
            internal = internal - 100;
          }
    
          setTimer(internal);
        }, 1000);
        return () => clearTimeout(interval2);
      }, [timer]);
    
      return (
        <div>
          <div>{timer}</div>
          <LinearProgress color="secondary" value={timer} variant="determinate" />
        </div>
      );
    };
    
    TotalProfit.propTypes = {
      className: PropTypes.string
    };
    
    export default TotalProfit;
    

    As explained here, the browser allocates less ressources to not focused tabs, so timers can be wrong. So one of the solution given isto use a timer initialized when your components is first render. Then you use the difference between Date.now() and your first render time to have your interval (modulo 100) (sandbox).

    import React, { useEffect, useState } from "react";
    import PropTypes from "prop-types";
    import { makeStyles } from "@material-ui/styles";
    import { LinearProgress } from "@material-ui/core";
    
    const TotalProfit = (props) => {
      const [timer] = useState(Date.now()/1000);
      const [delta, setDelta] = useState(0);
    
      useEffect(() => {
        const interval2 = setInterval(() => {
          setDelta((Date.now()/1000 - timer) % 100);
        }, 1000);
        return () => clearInterval(interval2);
      }, [timer]);
    
      return (
        <div>
          <div>{delta}</div>
          <LinearProgress color="secondary" value={delta} variant="determinate" />
        </div>
      );
    };
    
    TotalProfit.propTypes = {
      className: PropTypes.string
    };
    
    export default TotalProfit;
    

    Else if your goal is only to have a loader you can use a css animation as shown (slightly modified to get the same render you get with js) here:

    body {margin: 0; padding: 0;}
    @keyframes loader-animation {
      0% {
        width: 0%;
      }
      100% {
        width: 100%;
        left: 0%
      }
    }
    .loader {
      height: 5px;
      width: 100%;
    }
    .loader .bar {
      position: absolute;
      height: 5px;
      background-color: dodgerblue;
      animation-name: loader-animation;
      animation-duration: 3s;
      animation-iteration-count: infinite;
      animation-timing-function: ease-in-out;
    }
    <div class="loader">
      <div class="bar"></div>
    </div>