0

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

Peter Slanina
  • 196
  • 1
  • 2
  • 9

1 Answers1

1

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>
Nico_
  • 1,388
  • 17
  • 31
  • Hey, I understand the issue , however couldn't fix it by changing the code as you suggested. Why does the leak manifest itself only if tab is not focused? – Peter Slanina Aug 12 '20 at 16:02
  • Can you tell me if you can reproduce the issue in the sandbox I added to my answer? I tried but cannot reproduce it. My guess for the issue occurring while the tab gains focus back would be because of some memory management of the browser (limiting some renders while the user is not watching the tab). – Nico_ Aug 12 '20 at 16:42
  • yes the issue is still there for me. Opened the app at 17:54:00 , changed tab to my facebook , returned at 17:54:55 - and the bar was showing timer = 50 ( progress bar in the middle ) - as I said issue doesn't happen when my active tab is the sandbox. can you reproduce it ? – Peter Slanina Aug 12 '20 at 16:56
  • Just seen you said you cannot reproduce it.... thanks ! – Peter Slanina Aug 12 '20 at 16:58
  • I tried a slightly different version and find out I had the bug too (but thought it was not that because of the `internal = internal - 1.695 * -1;`. I found [another question](https://stackoverflow.com/questions/5927284/how-can-i-make-setinterval-also-work-when-a-tab-is-inactive-in-chrome) on stackoverflow about the same issue. The solution would be to use `requestAnimationFrame`. I don't know the context of your code, but if you only want to fill a line and reset it once full, you can do that only with [css](https://stackoverflow.com/a/30981711/3672560). I updated my answer with this infos. – Nico_ Aug 13 '20 at 07:43