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!