TypeOfNaN

How to prevent useEffect from running on mount in React

Nick Scialli June 28, 2021🚀 3 minute read

no useEffect on initial render

Sometimes we don’t want a useEffect hook to run on initial render. This could be for many reasons, but a common use case is when we’re fetching the data on which that hook depends. Initially, the data is empty, so running that hook is pointless.

A theoretical example

In the following component, we fetch some data and then we intend to doSomething with that data after it’s been fetched.

function MyComponent() {
  const [data, setData] = useState();

  // An effect to fetch the data
  useEffect(() => {
    fetch('/api/some-api')
      .then((res) => res.json())
      .then((d) => {
        setData(d);
      });
  }, []);

  // Do something else with the data
  useEffect(() => {
    doSomething(data);
  }, [data]);
}

On initial render, we fetch the data, but we also run our second effect.

Important: the useEffect hook will always run on mount regardless of if there is anything in its dependency array.

We probably don’t want to actually run this effect on our data when it’s undefined (as it will be on initial render) but rather we want to wait until it is populated from the API call.

useRef to the rescue!

One solution to this problem is employing the useRef hook to track whether the component has mounted or not. While a ref is often thought of as a way to gain reference to DOM elements, it can also be used to refer to other objects and primitives.

In this case, we’ll create a ref to a boolean that tracks whether the component has mounted. It will start out as false, but once the effect runs for the first time, we can change it to true.

This is probably much easier to understand as code:

function MyComponent() {
  const [data, setData] = useState();
  const isMounted = useRef(false);

  // An effect to fetch the data
  useEffect(() => {
    fetch('/api/some-api')
      .then((res) => res.json())
      .then((d) => {
        setData(d);
      });
  }, []);

  // Do something else with the data
  useEffect(() => {
    if (isMounted.current) {
      doSomething(data);
    } else {
      isMounted.current = true;
    }
  }, [data]);
}

isMounted.current starts off as false, so the first runthrough of our second useEffect hook won’t call doSomething. Instead, it will set isMounted.current to true. On subsequent runs of the hook, isMounted.current will be true and doSomething will be executed on our data.

Why this approach?

There are a few nice things about using a ref for this purpose:

  • We can be assured that on any subsequent run of the hook, our data will have been changed—it’s the only value in the dependency array.
  • By not using state to track isMounted, we’re not forcing extra component renders. isMounted.current = true is mutating our ref (which is its intended use), and therefore it’s the same object/won’t cause re-renders.

Conclusion

The useEffect hook can be particularly challenging. By understanding the useRef hook, we can gain a bit more control about when our effects are run.

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli is a software engineer at the U.S. Digital Service.