The UpgradeJS Blog

Why your useEffect is firing twice in React 18

React 18 has been out for about 8 months now and it contains a ton of goodies for both end-users and library authors. However, there is one new change that seems to keep coming up in GitHub issues and forum posts – useEffect now fires twice under Strict Mode in development. In this post, I’ll briefly go over exactly what Strict Mode in React is and then offer some advice on dealing with the change – as well as a few useEffect best practices.

What is Strict Mode?

In all likelihood, you’re already using Strict Mode in React. It’s on by default when you bootstrap a project with something like create-react-app opens a new window or create-next-app opens a new window . If you take a look at your index.js file in a create-react-app project, you’ll see something like this:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  // this is setting your entire app to Strict Mode
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

When Strict Mode is enabled, React will raise warnings in development whenever it comes across certain code or conventions that are problematic or outdated. If you were around when Hooks were introduced, you might recognize this Strict Mode warning about old class-based lifecycle methods:

Warning: componentWillMount has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.

* Move code with side effects to componentDidMount, and set initial state in the constructor.
* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.

In addition to flagging the UNSAFE_ lifecycle methods (the other two are componentWillReceiveProps and componentWillUpdate), Strict Mode will warn you about using:

  • the legacy string ref API
  • the deprecated findDOMNode method
  • the legacy context API

Strict Mode also includes a feature to help you detect unexpected side effects. To help surface these issues, React intentionally double-invokes these functions in development:

  • constructor, render, and shouldComponentUpdate
  • getDerivedStateFromProps
  • Function component bodies
  • Functions passed to useState, useMemo, and useReducer

This behavior was present in React 17, but you might not have noticed it because React would dedupe console.log calls. You will probably notice it now, however, because that behavior is gone in React 18.

Note: The TL;DR for double-invoking is that React does stuff in two phases – render, where it determines what has to change and commit, where it applies those changes. Since the render phase can be slow, React will eventually break this phase into pieces. This means that parts of this phase may be repeated to ensure the state is correct and up to date. Since any of the above functions could actually be called twice in production, the double-invoking will highlight code that is not idempotent (which means that for a given input, your function will always return the same output – or “your function always does the same thing, no matter how many times it’s called”).

Now that we know what Strict Mode is (and the lede has been sufficiently buried), let’s figure out what’s got everyone so riled up.

The new Strict Mode feature

To get to the point – in development mode in React 18, your components will unmount and remount whenever they are mounted for the first time. In practice, this means the following functions will also be called twice:

  • componentDidMount
  • componentWillUnmount
  • useEffect
  • useLayoutEffect
  • useInsertionEffect (this one is for library authors, but I’m including it for completeness)

The React docs have a decent explanation opens a new window of why they added this behavior, but it boils down to ensuring any functions that have side effects also have adequate cleanup. This comment from React maintainer, Dan Abramov, sums it up pretty nicely:

The mental model for the behavior is “what should happen if the user goes to page A, quickly goes to page B, and then switches to page A again”. That’s what Strict Mode dev-only remounting verifies.

If your code has a race condition when called like this, it will also have a race condition when the user actually switches between A, B, and A quickly. If you don’t abort requests or ignore their results, you’ll also get duplicate/conflicting results in the A -> B -> A scenario.

Once again – this is only in development. This behavior is not present in your production build. If you don’t want this feature, you can disable Strict Mode by removing the <React.StrictMode> tags from your index.js file. However, this is not recommended by the React team – the best course of action is to handle these cases where user behavior could cause your components to rapidly unmount and remount. So how can we do that?

Clean up your useEffect

Basically, we just need to make sure we return a cleanup function from useEffect when applicable. For example, if you have an event listener that is set up in a useEffect, make sure you remove it on unmount:

useEffect(() => {
  window.addEventListener("resize", handleEvent);

  return () => {
    window.removeEventListener("resize", handleEvent);
  };
}, []);

Another common case is doing asynchronous work inside useEffect. Let’s say we make some async call and then set state with the eventual return value. If the component unmounts before the promise resolves, that could represent a possible memory leak. In order to avoid this, we can use a boolean flag to set the state only when the component is actually mounted:

useEffect(() => {
  // set flag to `true` on mount
  let mounted = true;

  const getData = async () => {
    const response = await somePromise();
    // check flag before setting state
    if (mounted) {
      setData(response);
    }
  };
  getData();

  return () => {
    // set to false on unmount
    mounted = false;
  };
}, []);

If your async function is fetching something, you can also use an AbortController:

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

  const getData = async () => {
    try {
      const response = await fetch(someUrl, {
        signal: abortController.signal, // add signal to fetch call
      });
      const json = await response.json();
      // similar to `mounted` check, confirm
      // we haven't unmounted before setting state
      if (!abortController.signal.aborted) {
        setData(json);
      }
    } catch (error) {
      console.log(error);
    }
  };
  getData();

  return () => {
    // cancel the fetch
    abortController.abort();
  };
}, []);

In both cases, you may still see two requests happening in your network logs in production – that’s expected. In the first case, we don’t actually cancel the call – we just don’t set state if the component unmounts. In the second case, that’s just how AbortController works.

It should also be noted that these two examples are somewhat naive in that there are typically more things to consider (for example, the AbortController.abort() method throws an error that you should probably handle). If managing this kind of logic becomes too complex, you can always reach for some battle-tested libraries that have already figured this out for us. These are a few of the more popular ones:

Recap

While React 18 brings a bunch of new features, the new Strict Mode behavior seems to have stolen the spotlight a little bit. However, there’s no need to fear because it’s working as intended and helping us catch bugs before we push to production. As long as we handle the clean up of our useEffect calls, we should be good to go! And if you’d like to read more about some of the other new things in React, you can check out their official announcement post opens a new window .