oderfla
oderfla

Reputation: 1797

Where to put setInterval in react

I need every 5 seconds (actually every 2 minutes, but having 5 seconds while testing) to make a GET request to an API. This should be done only when the user is logged in. The data should be displayed by one component that is called "Header". My first thought was to have an interval inside that component. So with "useEffect" I had this in the Header.js:

const [data, setData] = useState([]);

useEffect(async () => {
  const interval = setInterval(async () => {
    if(localStorage.getItem("loggedInUser")){
      console.log("Logged in user:");
      console.log(new Date());
      try{
        const data = await getData();
        if(data){
          setData(data);
        }
        console.log("data",data);
      }
      catch(err){
        console.log("err",err);
      }
    }
  }, 5000);
  return () => clearInterval(interval);
}, []);

Im getting the data. But looking at the console, it does not look so good. First of all, sometimes I get this:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, 
but it indicates a memory leak in your application. To fix, cancel all subscriptions 
and asynchronous tasks in a useEffect cleanup function.

It seems to happen when I go to other pages that have been called with a "href" (so without using for example history to change page). Also, probably because of the thing above, after a while that I browse the pages, I see that the interval is not every 5 seconds anymore. It happens 2-3 times every 5 seconds. Maybe every seconds. To me this does not look good at all. Im wondering first of all if this component is the right place to put the interval in. Maybe the interval should be in App.js? But then I would need a mechanism to pass data to children component (the Header.js, maybe could do this with Context).

Upvotes: 0

Views: 825

Answers (2)

Stefan J
Stefan J

Reputation: 1687

As Sergio stated in his answer on async actions you should aways check if the component is currently mounted before doing side effects.

However using a variable that is initialised within the component won't work as it is being destroyed on unmount. You should rather use useRef hook as it will persist between component mounts.

const [data, setData] = useState([]);
const mountedRef = useRef(null);

useEffect(() => {
  mountedRef.current = true;

  return () => {
    mountedRef.current = false;
  };
}, []);

useEffect(async () => {
  const interval = setInterval(async () => {
    if (localStorage.getItem("loggedInUser")){
      console.log("Logged in user:", new Date());

      try {
        const data = await getData();

        if (data && mountedRef.current) {
          setData(data);
          console.log("data", data);
        }
      } catch(err) {
        if (mountedRef.current)  console.log("err", err);
      }
    }
  }, 5000);
  return () => clearInterval(interval);
}, []);

Or alternatively declare a boolean variable in the module scope so that its not freed by the garbage collection process

Upvotes: 2

Sergio Tx
Sergio Tx

Reputation: 3866

You can create a isMounted variable to control if the async task happens after the component is unmounted.

As the error said, you are trying to update component state after it's unmounted, so you should "protect" the setData call.

const [data, setData] = useState([]);

useEffect(async () => {
  let isMounted = true;
  const interval = setInterval(async () => {
    if(localStorage.getItem("loggedInUser")){
      console.log("Logged in user:");
      console.log(new Date());
      try{
        const data = await getData();
        if(data && isMounted){
          setData(data);
        }
        console.log("data",data);
      }
      catch(err){
        console.log("err",err);
      }
    }
  }, 5000);
  return () => {
    clearInterval(interval);
    isMounted = false;
  }
}, []);

Upvotes: 1

Related Questions