Why does React 18 double render useEffect in development?

ReactJavaScriptFrontendReact Hooks

React 18 brings a lot of new goodies, but what causes a lot of head-scratching is how components using useEffect seem to mount and unmount twice during development — specifically in StrictMode.

This is intended behavior

React intentionally double-invokes certain lifecycle methods in development to help surface bugs related to mounting and unmounting. It does not happen in production.

Repeat after me:

  • It's not going to be there during production
  • It helps discover bugs in mounting/unmounting during development
  • Dan Abramov said it's cool. And Dan is cool. 😎

useEffect

React 18 double rendering with empty dependency array

So what happens when you do this?

import { useEffect } from "react";

export default function App() {
  useEffect(() => {
    console.log(`I'm mounting!`);
    return () => console.log(`And now I'm unmounting`);
  }, []);

  return <h1>Hello SPD Readers!</h1>;
}

You'll see this output twice in development. Why? Because React simulates a mount/unmount/remount cycle.

React is stress-testing your component

This helps ensure your component doesn't break when mounted and unmounted quickly, like in route transitions.


Want to turn it off?

You technically can — but you shouldn't.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const root = createRoot(document.getElementById("root"));

root.render(
  // <StrictMode>
  <App />
  // </StrictMode>
);
Don't do this in real projects

Removing <StrictMode> just to avoid double renders defeats the purpose of development safety checks.


How to fetch data correctly in React 18

Fetching data in useEffect without a framework or a cache? You might see double requests. Here's what Dan Abramov recommends:

Use framework-level data fetching

If you're using a framework (like Next.js or Remix), leverage their data loading mechanisms. They decouple rendering from fetching.

If you're not using a framework:

  • Use a client caching library (e.g., React Query, Apollo)
  • Avoid raw useEffect data fetching if possible
useEffect(() => {
  const controller = new AbortController();

  fetch("/api/data", { signal: controller.signal })
    .then((res) => res.json())
    .then(setData);

  return () => controller.abort();
}, []);
Double fetch is harmless if you cache

With client caching, re-fetching doesn't result in broken behavior or wasted performance.


In summary

  • Double mount/unmount in dev is intentional with StrictMode
  • It helps catch bugs earlier in development
  • Only happens in development, not production
  • Use the right tool for data fetching (framework or cache)
React is a library, not a framework

You're responsible for picking the right tools around React. React Query or RTK Query are great for data fetching.


The Goodbye 👋

Hope this cleared things up! If you liked the article or have questions, drop a message on my socials.

Thanks for reading!