When Exactly is the useEffect Hook Called in React?

Last updated on December 27, 2022
When Exactly is the useEffect Hook Called in React?

The useEffect hook is called in a component after the first render and every time the component updates. By the timer useEffect is called, the real DOM would have been updated to reflect any state changes in the component.

Let's take a look at a quick example to practically observe this behavior:

import { useEffect, useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Button Clicks: ${count}`;
  }, [count])

  const handleAdd = () => {
    setCount((count) => count + 1);
  }

  return (
    <div>
      Count: {count}
      <br />
      <button onClick={handleAdd}>Add</button>
    </div>
  );
}

The first time the component renders from page loading, useEffect is called and uses the document.title property to set the page title to a string whose value depends on a count state variable.

The page title is changed from useEffect being called after the component renders.
The page title is changed from useEffect being called after the component is rendered.

If you watch closely, you'll see that the page title was initially "Coding Beauty React Tutorial", before the component was added to the DOM and the page title was changed from the useEffect call.

useEffect will also be called when the state is changed:

The page title is changed from useEffect being called after the component updated.
The page title is changed from useEffect being called after the component is updated.

As you might know, useEffect accepts an optional dependency array as its second argument. This array contains the state variables, props, and functions that it should watch for changes.

In our example, the dependency array contains only the count state variable, so useEffect will only be called when the count state changes.

useEffect called twice in React 18?

React 18 introduced a new development-only check to Strict Mode. This new check automatically unmounts and remounts a component when it mounts for the first time, and restores the previous state on the second mount.

Note: The check was added because of a new feature that will be added to React in the future. Learn more about it here.

This means that the first render causes useEffect to actually be called two times, instead of just once.

Here's an example that lets us observe this new behavior.

import { useEffect, useState } from 'react';

export default function App() {
  const [time, setTime] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setTime((prevTime) => prevTime + 1);
    }, 1000);
  }, []);

  return (
    <div>
      Seconds: {time}
    </div>
  );
}

It's a basic time-counting app that implements the core logic that can be used to build timer and stopwatch apps.

We create an interval listener with setInterval() that increments a time state by 1 every second. The listener is in a useEffect that has an empty dependency array ([]), because we want it to be registered only when the component mounts.

But watch what happens when we check out the result on the web page:

The seconds go up by 2 every second.
The seconds go up by 2 every second.

The seconds are going up by 2 every second instead of 1! Because React 18's new check causes the component to be mounted twice, an useEffect is called accordingly.

We can fix this issue by unregistering the interval listener in the useEffect's cleanup function.

useEffect(() => {
  const timer = setInterval(() => {
    setTime((prevTime) => prevTime);
  });

  // 👇 Unregister interval listener
  return () => {
    clearInterval(timer);
  }
}, [])

The cleanup function that the useEffect callback returns is called when the component is mounted. So when React 18 does the compulsory first unmounting, the first interval listener is unregistered with clearInterval(). When the second interval listener is registered on the second mount, it will be the only active listener, ensuring that the time state is incremented by the correct value of 1 every second.

The second go up by 1 every second - success.
The seconds go up by 1 every second - success.

Note that even if we didn't have this issue of useEffect being called twice, we would still have to unregister the listener in the cleanup function, to prevent memory leaks after the component is removed from the DOM.

See also