React.StrictMode
React.StrictMode is a development-only React wrapper that double-invokes renders and effects to expose side effects and unsafe patterns before they reach production.
Theory
TL;DR
- Think of it as a flight simulator: it stresses your components twice during dev to catch weaknesses before real users hit them
- Renders components twice and runs effects via setup-cleanup-setup in development; production builds strip it entirely via tree-shaking
- React 18+ added the effect double-mount to catch cleanup bugs that concurrent features expose
- New project: wrap
<App />from day one. Legacy code: add StrictMode subtree by subtree.
Quick example
// index.js
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
function Counter() {
const [count, setCount] = useState(0);
console.log('Render'); // Logs twice in dev
useEffect(() => {
console.log('Effect ran'); // Logs twice on mount in dev
});
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Counter />
</React.StrictMode>
);
// Dev: "Render" x2, "Effect ran" x2
// Production: "Render" x1, "Effect ran" x1The double logs are not a bug. They are exactly what StrictMode is for.
What StrictMode checks
StrictMode catches four categories of problems:
- Impure render functions: anything mutating state or external values during render fires twice, so the mutation becomes obvious
- Deprecated lifecycle methods:
componentWillMount,componentWillReceiveProps,componentWillUpdateall produce console warnings - Legacy string refs:
ref="button"style refs, deprecated since React 16 and removed in React 19 - Non-idempotent effects (React 18+): effect setup and cleanup run in sequence (setup, cleanup, setup) to verify that cleanup properly reverses what setup did
How the double-invoke works
React's fiber reconciler wraps targeted components in a dev-only path inside ReactFiberStrictMode.js. During development it calls render, useState initializers, useMemo callbacks, and useEffect setup/cleanup twice. The second invocation's DOM mutations are discarded before they reach the browser. The fiber tree is shared between both passes; React replays the function calls and throws away the second pass output. No visible overhead reaches the user.
For useEffect in React 18+, the sequence on mount is: setup runs, cleanup runs, setup runs again. This simulates what concurrent features like Transitions do when React unmounts and remounts a component to reclaim resources. If cleanup does not properly reverse setup, the third invocation fails in obvious ways during development rather than in production.
Key difference from production
StrictMode does not change what your components render or how your app behaves for users. It changes how many times React calls your functions internally during development. Production builds strip the StrictMode wrapper entirely, so the double invocations never happen outside dev.
When to use
- New React 18+ project: wrap
<App />from day one. Vite and Create React App do this by default. - Migrating a legacy codebase: add StrictMode to one subtree at a time, fix warnings, then expand coverage.
- Third-party library that spams warnings you cannot fix: leave that component outside the wrapper and report the issue upstream.
// Selective wrapping - Header and LegacyWidget stay outside
function App() {
return (
<div>
<Header />
<React.StrictMode>
<Sidebar />
<Content />
</React.StrictMode>
<LegacyWidget />
</div>
);
}Common mistakes
Treating double logs as a bug. The dev console shows "Render" twice. That is correct. Production shows it once. The fix is not removing StrictMode.
Mutating shared objects inside render. This is exactly what StrictMode is designed to catch.
// Bug: shared object mutated during render
const config = { count: 0 };
function BadComponent() {
config.count++; // Runs twice in dev, so count is 2 after the first visible render
return <div>{config.count}</div>;
}
// Fix: derive value from state
function GoodComponent() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}Effects that do not clean up properly. React 18+ StrictMode runs cleanup before the second setup. Incomplete cleanup breaks the second pass.
// Bug: two intervals fire on mount in dev because cleanup is missing
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
// no cleanup
}, []);
// Fix: cleanup makes the effect idempotent
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);Removing StrictMode to suppress third-party warnings. This hides real issues without fixing them. Move the noisy library outside the wrapper and report the bug upstream.
Expecting StrictMode behavior in Jest tests. Jest renders once by default. Wrap explicitly to test under StrictMode conditions.
// setupTests.js
import React from 'react';
import { render } from '@testing-library/react';
export function renderStrict(ui) {
return render(<React.StrictMode>{ui}</React.StrictMode>);
}Real-world usage
- Vite + React and Create React App: both default templates wrap the root with
<StrictMode> - Next.js: opt in via
reactStrictMode: trueinnext.config.js; off by default - Storybook: wrap stories in StrictMode to catch HOC issues before they reach the app
- Redux Toolkit: double-invocations are safe because RTK selectors are pure functions by design
One pattern that shows up often in codebases: a fetch fires twice in development and the team removes StrictMode instead of adding an AbortController. Adding the controller fixes both the StrictMode behavior and a real memory leak that was already there in production, just invisible.
Follow-up questions
Q: Why does React 18+ StrictMode run effects twice on mount?
A: It simulates concurrent features where React may unmount and remount a component to reclaim resources. If setup runs twice without cleanup neutralizing the first run, you get real bugs in concurrent mode, not just in dev.
Q: Does StrictMode slow down production?
A: No. React strips it entirely in production via tree-shaking. The double invocations exist only in dev builds.
Q: A third-party library logs warnings in StrictMode. What should I do?
A: Check if a newer version supports React 18. If not, wrap that component outside StrictMode and file a bug with the library author. Do not disable StrictMode for the whole app.
Q: What replaced string refs?
A: useRef in function components, React.createRef() in class components. String refs are removed in React 19.
Q (senior): How does StrictMode interact with the React Compiler in canary builds?
A: The React Compiler auto-memoizes components by assuming renders are pure. StrictMode's double-invoke validates that assumption: if a component returns different output on the second render, the compiler cannot safely cache it. So StrictMode surfaces the impure functions that the compiler would otherwise mis-optimize without any warning.
Examples
Pure vs impure render under StrictMode
// Pure: survives double-render, both logs are identical
function Greeting({ name }) {
const msg = `Hello, ${name}`; // computed from props only
console.log(msg); // "Hello, World" x2 in dev - both identical
return <div>{msg}</div>;
}
// Impure: reveals mutation bug on double-render
let calls = 0;
function ImpureGreeting({ name }) {
calls++; // mutates external variable during render
console.log(`Call #${calls}: Hello, ${name}`);
// Dev output: "Call #1: Hello, World" then "Call #2: Hello, World"
// calls is 2 after one visible render - a real bug
return <div>Hello, {name}</div>;
}Greeting logs the same string twice because it is pure. ImpureGreeting makes the mutation visible: calls ends up at 2 after what the user sees as one render. That is the whole point.
Fetch with AbortController cleanup
// UserList.jsx
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch('https://jsonplaceholder.typicode.com/users', {
signal: controller.signal,
})
.then(res => res.json())
.then(data => setUsers(data))
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // cleanup cancels the first request
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Without the AbortController, StrictMode fires two requests on mount in dev and both try to call setUsers. With cleanup, the first request is cancelled and only the second completes. Production fires one request. Same code, correct behavior in both environments.
Deprecated lifecycle methods - before and after
// Before: triggers StrictMode warnings
class UserProfile extends React.Component {
componentWillMount() {
// Warning: use componentDidMount instead
this.setState({ loading: true });
}
componentWillReceiveProps(nextProps) {
// Warning: use getDerivedStateFromProps instead
if (nextProps.userId !== this.props.userId) {
this.setState({ loading: true });
}
}
render() {
return <div>{this.state.loading ? 'Loading...' : this.props.userId}</div>;
}
}
// After: StrictMode-clean class component
class UserProfile extends React.Component {
static getDerivedStateFromProps(props, state) {
if (props.userId !== state.prevUserId) {
return { loading: true, prevUserId: props.userId };
}
return null;
}
componentDidMount() {
this.loadUser(this.props.userId);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.loadUser(this.props.userId);
}
}
loadUser(id) {
// fetch logic here
this.setState({ loading: false });
}
render() {
return <div>{this.state.loading ? 'Loading...' : this.props.userId}</div>;
}
}componentWillMount and componentWillReceiveProps are marked UNSAFE_ since React 16.3 and will be removed in a future major version. StrictMode makes this visible now so you migrate before the deadline.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.