How to use useEffect and setInterval correctly

Posted by ganesan on Wed, 02 Feb 2022 22:22:03 +0100

background

useEffect seems simple, but it's not. Because I didn't understand the working principle of useEffect, I was severely cheated.

The thing is this: I want to achieve a very simple "object falling" effect:

  • The vertical position of the object is represented by the state of position y. calculated from top to bottom, the larger the position Y, the farther away from the top of the screen;
  • Set a setInterval, add position Y every short period of time (30 ms), and then render the object based on position Y, so as to achieve the effect of "object falling";

Seemingly very simple, but encountered unexpected difficulties!

Failed attempt

I think, since the function in setInterval will trigger every other period of time, setInterval obviously only needs to be run once!

As we all know, the way to run a function only once is to use the useEffect hook whose second parameter is an empty array. So I wrote the following version of the code:

function App() {
  const [positionY, setPositionY] = useState(0)

  useEffect(() => {
    timer = setInterval(() => {
      // Stop falling when falling below the screen
      if (positionY < WINDOW_HEIGHT) {
        setPositionY(positionY + 5)
      }
    }, 30)
    
    return () => {
        clearInterval(timer);
    };
  }, []);

  return (
  	// slightly
}

As a result, I was stupid as soon as I ran: the object was not moving at all! To be exact, it just moved a little, and then it remained still and didn't understand

What went wrong?

Why does it fail like that? Then I finally found the reason:

  • When App() is run for the first time, useEffect is run, setInterval is set, and setInterval does trigger setpositiony (position Y + 5), so the object will have the first "slight movement";
  • However, after the position is updated, React will call App() again with the updated state, but the function in useEffect will not run this time;

The result is that the function in setInterval always gets the initial value of position y.

Why is that? Because setInterval was created when App() was first run, the position in that App() never changed.

The updated position Y is indeed used to run App() for the second time, but App() will not run setInterval for the second time.

What a surprise!

Correct writing

There are at least two correct ways to write it. Examples are as follows.

Method 1

The first method is:

  • Execute useEffect every time the position Y changes, and use the latest position Y to create a new setInterval;
  • At the same time, the previous setInterval will be automatically cleared, and there will only be one setInterval timer;

Examples are as follows:

function App() {
  const [positionY, setPositionY] = useState(0)

  useEffect(() => {
    timer = setInterval(() => {
      if (positionY < WINDOW_HEIGHT) {
        setPositionY(positionY + 5)
      }
    }, 30)
    
    return () => {
    	// Each time a new useEffect is executed, the previous useEffect will be cleaned up
    	// This function will be called to clear the previous setInterval timer
        clearInterval(timer);
    };
  }, [positionY]); // This ensures that useEffect will be executed every time the position Y is changed

  return (
  	// slightly
}

Method 2

The second method is:

  • setState can accept a function whose first parameter is the latest state. In this way, you can ensure that you get the latest state;

Examples are as follows:

function App() {
  const [positionY, setPositionY] = useState(0)

  useEffect(() => {
    timer = setInterval(() => {
      // This will ensure the latest position y
      setPositionY(oldVal => {
        if (oldVal < WINDOW_HEIGHT) {
          return oldVal + 5
        }
        return oldVal
      })
    }, 30)
    
    return () => {
        clearInterval(timer);
    };
  }, []); // You only need to execute useEffect once

  return (
  	// slightly
}

Reference link

useState set method not reflecting change immediately

Topics: Javascript React