Why does React 18 double render useEffect in development?
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.
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. ๐

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.
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>
);
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:
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();
}, []);
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)
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!