StrictMode in React
TL;DR:
StrictModeis a development-only React feature that finds common bugs early. It double-renders your components, re-runs Effects, and warns about deprecated APIs. It does nothing in production. Keep it on.
I've been confused about StrictMode in React for a long time. Why does it even exist? Why have I never seen any projects use it?
Next.js suggests enabling it by default, so I think it's time to discuss it a little more seriously.
#What is StrictMode?
StrictMode is a component provided by React, and as the React docs describe:
<StrictMode>lets you find common bugs in your components early during development.
The new version of the React docs does a great job explaining it, so you might want to read it here.
When you develop locally, you wrap your app like this:
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode> );
That's it. No extra setup. No production config. StrictMode only runs extra checks in development. In production, it is a no-op.
You can also wrap just part of the tree if you want to adopt it gradually:
function App() { return ( <> <Header /> <StrictMode> <Dashboard /> </StrictMode> <Footer /> </> ); }
#Why does my useEffect run twice?
This is the question that made me hate StrictMode the first time I enabled it.
You write a normal fetch:
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${userId}`) .then((res) => res.json()) .then(setUser); }, [userId]); return <div>{user?.name}</div>; }
Then you open the Network tab and see two requests. You think React is broken. You Google it. Half the answers say "just turn off StrictMode."
Please don't.
That second run is the point. In development, React mounts, unmounts, then mounts again. It is simulating a future where your component can mount, go away, and come back (think Strict Mode today, Concurrent Features tomorrow). If your Effect is not safe to start twice, you already have a bug. You just haven't shipped it yet.
The fix is a cleanup function, not disabling the check:
useEffect(() => { const controller = new AbortController(); fetch(`/api/users/${userId}`, { signal: controller.signal }) .then((res) => res.json()) .then(setUser) .catch((error) => { if (error.name !== 'AbortError') { throw error; } }); return () => controller.abort(); }, [userId]);
Same story for subscriptions, timers, and event listeners. If you open a socket, close it. If you add a listener, remove it. StrictMode is just forcing you to write the cleanup you already needed.
#What StrictMode actually checks
In development only, React will:
- Re-render an extra time to find impure rendering
- Re-run Effects an extra time to find missing cleanup
- Re-run ref callbacks an extra time to find missing ref cleanup
- Warn about deprecated APIs
None of this happens in production. Your users never pay for these extra renders.
#Impure rendering
This one is sneaky. A render function must be pure: same props and state in, same UI out. If you mutate something during render, the extra render in StrictMode will expose it immediately.
function StoryTray({ stories }) { const items = stories; items.push({ id: 'create', label: 'Create Story' }); return ( <ul> {items.map((story) => ( <li key={story.id}>{story.label}</li> ))} </ul> ); }
With StrictMode on, "Create Story" shows up twice. Without it, this can look fine until some later re-render quietly corrupts the list.
The fix is to stop mutating:
function StoryTray({ stories }) { const items = [...stories, { id: 'create', label: 'Create Story' }]; return ( <ul> {items.map((story) => ( <li key={story.id}>{story.label}</li> ))} </ul> ); }
#Missing Effect cleanup
I already showed the fetch case. The same pattern shows up everywhere:
useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); }, []);
useEffect(() => { const onResize = () => setWidth(window.innerWidth); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []);
If you forget the cleanup, StrictMode will subscribe twice in development. That is a gift. In production, the same leak shows up when a user navigates away and back, or when a parent remounts the tree.
#Deprecated APIs
StrictMode also warns when you still use old React APIs that are going away. Find them in development, not after a major upgrade breaks your Friday deploy.
#How to enable it
Create React App / Vite / plain React: wrap the root, as shown above.
Next.js App Router: enabled by default since Next.js 13.5.1.
Next.js Pages Router: turn it on in next.config.js:
module.exports = { reactStrictMode: true, };
You can still set reactStrictMode: false if you want. I would only do that as a temporary escape hatch while you fix the real bugs.
#Should you turn it off?
I used to think yes. Double fetching looked noisy. Logs looked messy. Some third-party libraries were not ready.
Then I shipped a page that opened two WebSocket connections every time a user visited it. The bug was a missing cleanup. StrictMode would have caught it on my laptop.
So my current rule is:
- Keep
StrictModeon. - If an Effect runs twice, add cleanup or make it idempotent.
- If a library breaks under Strict Mode, that library has a remount bug. File an issue or wrap only the part you control.
- Never disable it just to make the Network tab look quieter.
#That's it
StrictMode is not a linter, not a type system, and not a silver bullet. It is a cheap way to pretend your component will remount, so you notice impure renders and missing cleanups before users do.
Enable it. Read the warnings. Fix the bugs. Then forget it exists, because in production it already forgot about you.
Happy coding! 🚀