You click a button, and a quick log shows your React component rendered twice. It feels like a bug, and sometimes it points to one, but often it’s React doing exactly what it’s told. Knowing which is which saves hours of chasing ghosts.

Rule out Strict Mode first
In development with Strict Mode on, React intentionally renders components twice to surface side effects that aren’t safe. This only happens in development and disappears in production. Before debugging, confirm whether you’re seeing a real double render or Strict Mode’s deliberate one.
The React documentation on Strict Mode lists what gets doubled: component bodies, state initializers and reducers run twice, and effects run setup, cleanup and setup again when a component mounts. That last part is why a fetch inside useEffect can appear twice in the Network tab during development. It is a test, not a fault.
The quickest check is a production build. Run it locally, repeat the click, and count again. If the double render is gone, Strict Mode was the cause and your code is fine. Do not switch Strict Mode off to make the logs quieter. It is catching effects that would misbehave later, for example when a component unmounts and mounts again.
How to tell why React renders twice for real
If the extra render survives a production build, measure before you change anything. Guessing leads to useMemo on everything and no improvement.
- Open the React DevTools Profiler, record one click, and look at which components rendered and how many commits happened.
- Read the reason. The Profiler can record why each component rendered, such as changed props, changed state or a parent render.
- Turn on render highlighting in the DevTools settings, so components flash on screen as they render.
- Check the commit count. Two commits for one click points at state. One commit with many components points at structure.

Then look at state and props
Updating state in a way that triggers a second render is common: setting two separate state values in one handler, where one depends on the other, can cause an extra pass. Batch related state into a single update or a reducer so one event produces one render.
Since React 18, updates inside the same event handler are batched automatically. So in practice, the second pass usually comes from an effect. A useEffect watches one value and sets another, which means one render for the click and another for the effect. If the second value can be calculated from the first, calculate it during render and do not store it at all.
State that lives too high is another cause. When state sits in a parent, every change re-renders the whole subtree, even components that don’t use it. Move state down to where it’s actually needed, or split context so unrelated consumers don’t re-render together.
Context deserves a closer look. If a provider builds its value as a new object on every render, every component that reads that context renders again, even when nothing inside changed. Keep fast-changing values like form input in their own context, apart from slow ones like the signed-in user.
Unstable props are the third: passing a new object or function created inline on every render makes children re-render even when nothing changed. Memoize values and callbacks passed down. Rule out Strict Mode, then the fix is usually fewer, better-scoped state updates, not memoization sprinkled everywhere.
A typical example is style={{ margin: 8 }} or onSave={() => save(id)} written inline. Each render creates a fresh object or function, so a memoized child sees a new prop and renders anyway. Move constant objects outside the component, and wrap callbacks in useCallback only where a memoized child actually receives them.
Common mistakes
- Using
useCallbackwithoutmemo. A stable callback only helps if the child is wrapped inmemoor uses it as an effect dependency. - Random or index keys on lists that change. A key that changes forces React to throw the component away and mount it again, which is worse than a re-render.
- Treating every re-render as a problem. Renders are cheap. Fix the ones that are slow or that the user can feel.
If your project uses the React Compiler, much of the manual memoization is handled for you. It does not fix state that lives in the wrong place, though, so the advice above still applies.
Fewer renders by design
Some renders disappear when less of the page runs in the browser at all. In Next.js, keeping static parts as server components leaves fewer client components to re-render, as I explain in server components vs client components. The same thinking removes the classic login flash, covered in handling auth redirects in Next.js without the flicker.
Rendering surprises also look different outside a normal page. In a browser extension, the popup mounts fresh every time it opens, so state you expected to survive is gone. I cover that and other traps in building a Chrome extension with React. If a React app is fighting you on performance and you want it looked at properly, that is part of my web application development work.




