useEffect is running twice on mount in React

This is a normal behaviour since React 18 when you are in development with StrictMode. Here is an overview of what they say in the doc:

In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state.

With Strict Mode starting in React 18, whenever a component mounts in development, React will simulate immediately unmounting and remounting the component.

On the second mount, React will restore the state from the first mount. This feature simulates user behavior such as a user tabbing away from a screen and back, ensuring that code will properly handle state restoration.

This only applies to development mode, production behavior is unchanged.

It seems weird but at the end, it’s there so you write better React code, where each useEffect has its clean up function as soon as having two calls is an issue. Here are two examples:

/* Having a setInterval inside an useEffect: */

import { useEffect, useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setCount((count) => count + 1), 1000);

    /* 
       Make sure I clear the interval when the component is unmounted,
       otherwise I get weird behaviour with StrictMode, 
       helps prevent memory leak issues.
    */
    return () => clearInterval(id);
  }, []);

  return <div>{count}</div>;
};

export default Counter;
/* An API call inside an useEffect with fetch, almost similar with axios: */

useEffect(() => {
  const abortController = new AbortController();

  const fetchUser = async () => {
    try {
      const res = await fetch("/api/user/", {
        signal: abortController.signal,
      });
      const data = await res.json();
    } catch (error) {
      if (error.name === "AbortError") {
        /* 
          Most of the time there is nothing to do here
          as the component is unmounted.
        */
      } else {
        /* Logic for other cases like request failing goes here. */
      }
    }
  };

  fetchUser();

  /* 
    Abort the request as it isn't needed anymore, the component being 
    unmounted. Helps avoid among other things the well known "can't
    perform a React state update on an unmounted component" waring.
  */
  return () => abortController.abort();
}, []);

In this very detailed article called Synchronizing with Effects, React team explains useEffect as never before and says about an example:

This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back.

React verifies that your components don’t break this principle by remounting them once in development.

For your specific use case, you can leave it as it’s without any concern. But if you need to, saying you want useEffect‘s callback to only run when count changes, you can use a boolean with useRef to add some additional controls, like so:

import { useEffect, useRef, useState } from "react";

const Counter = () => {
  const countHasChangedRef = useRef(false);
  const [count, setCount] = useState(5);

  useEffect(() => {
    if (!countHasChangedRef.current) return;
    console.log("rendered", count);
  }, [count]);

  return (
    <div>
      <h1>Counter</h1>
      <div>{count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
          countHasChangedRef.current = true;
        }}
      >
        Click to increase
      </button>
    </div>
  );
};

export default Counter;

Lastly if you don’t want to deal with this development behaviour at all, you can remove that StrictMode component that wraps your App in index.js or index.tsx. For Next.js remove that reactStrictMode: true inside next.config.js.

However StrictMode is a tool for highlighting potential problems during development. And there is normally always a recommended workaround rather than removing it.

Leave a Comment