Tuesday, June 16, 2020

React - setInterval based on prop change with the useEffect hook

There are some times where you would like to setInterval and then clear it after a prop changes, say for example you want to display a different status/welcome message every second while you load some data (Postman/Discord-style).

So you want to set the interval whenever the data starts loading and clear it when it's done (to avoid memory leaks and unnecessary re-renders).

Let's go straight for the code, this is how to do it.

useEffect(() => {
  if (isLoading) {
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }
}, [isLoading]);

To see why this works we need to think a little backwards about how useEffect runs your code.

On the first render, this part executes, setting the interval and keeping a reference to the interval instance that's specific to this useEffect call.

useEffect(() => {
  if (isLoading) { // <-- true
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);

    /*
    return () => {
      clearInterval(interval);
    };
    */
  }
}, [isLoading]);

When our data is here and ready, the isLoading prop (or state, for that matter) becomes false, and when that happens, this part is executed first, clearing the current interval instance.

useEffect(() => {
  /*
  if (isLoading) {
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);
  */
    //return () => {
      clearInterval(interval);
    //};
  }
}, [isLoading]);

Then on the next render, the effect is run with the new value for isLoading, which will be false in our case

useEffect(() => {
  if (isLoading) { // <-- false
    /*
    const interval = setInterval(() => {
      setGreeting(getNextGreeting());
    }, 1000);

    return () => {
      clearInterval(interval);
    };
    */
  }
}, [isLoading]);

By wrapping the whole effect in a condition, we prevent setting a new interval when the condition is not met.

To sum it up, here's what happens

  • isLoading is true, the effect runs, sets interval.
  • isLoading gets updated to false, effect clears up the current interval first then goes for another run.
  • Condition is not met, don't set a new interval.

Also note that the effect listens only for changes in the isLoading prop/state, which is also a boolean (primitive value, can be safely shallow-compared for changes).

It can get a little tricky sometimes when you try to play around with React and the browser's imperative APIs. I hope this was clear enough.

As always, ask questions and leave notes, all is welcome.