Explain React rerendering through render work, reconciliation, commit, context fanout, and remount-vs-rerender identity bugs that make the DOM story easy to misread.
Use this React interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
When does React re-render a component, and when does it actually update the DOM?Frontend interview answer
This React interview question tests whether you can explain React rerenders vs DOM updates: bailouts, identity pitfalls, and debug mental models, connect it to production trade-offs, and handle common follow-up questions.
- React rerenders vs DOM updates: bailouts, identity pitfalls, and debug mental models explanation without falling back to memorized docs wording
- Rendering and State reasoning, edge cases, and production failure modes
- How you would answer the most likely React interview follow-up
Debug mental model
A React re-render means React re-ran part of the tree to compute the next UI. It does not automatically mean the browser DOM changed. That distinction matters in production: many rerender bugs are really identity bugs, context fanout, or remount problems, while many DOM-performance fears disappear once you separate render work from commit work.
Trigger | What happens | Important nuance |
|---|---|---|
State update (useState/useReducer) | Schedules a render for that component | If the new state is |
Parent re-renders | Children are rendered by default | Even if props “look the same”, React will call the child again unless you use |
Prop identity changes | Child re-renders | Inline objects/functions create new references every render → looks like “props changed” to memo/shallow compare. |
Context Provider value changes | All consumers may re-render | Context is identity-based too. |
Key / type changes at a position | React remounts the subtree | This is bigger than a re-render: local state resets because React treats it as a different component. |
Phase | What React does | Rule of thumb |
|---|---|---|
Render phase | Runs component functions / class | Must be pure (no subscriptions, network calls, DOM writes). React may run it more than once in dev/StrictMode. |
Reconciliation | Compares old vs new elements (type + key + position) and decides what to keep/move/mount/unmount | Stable keys/types preserve state and reduce work; unstable identity causes extra work and bugs. |
Commit phase | Applies DOM mutations and runs effects/lifecycles | This is when the browser DOM can actually change; effects run after commit. |
import React from 'react';
const Child = React.memo(function Child({ onInc, config }) {
console.log('Child rendered');
return <button onClick={onInc}>Inc</button>;
});
export default function Parent() {
const [count, setCount] = React.useState(0);
// ❌ NEW function/object every render -> Child sees "new props" -> re-renders
// const onInc = () => setCount(count + 1);
// const config = { step: 1 };
// ✅ Stable identities -> Child can skip renders via React.memo
const onInc = React.useCallback(() => setCount((c) => c + 1), []);
const config = React.useMemo(() => ({ step: 1 }), []);
console.log('Parent rendered');
return (
<div>
<p>Count: {count}</p>
<Child onInc={onInc} config={config} />
</div>
);
}
What this example proves
1) Updating count re-renders Parent.
2) By default, children render too.
3) React.memo lets a child bail out if props are shallow-equal, but only if you keep prop identities stable (callbacks/objects).
Before/after bug: memoized child still hot because prop identity changes
The usual production surprise is: “But the child is memoized.” If you keep allocating new objects/functions in the parent, the memo boundary sees changed props every time.
const Chart = React.memo(function Chart({ filters, onPointClick }) {
console.log('Chart rendered');
return <canvas />;
});
function Dashboard({ region }) {
// ❌ New references every render
return (
<Chart
filters={{ region, includeArchived: false }}
onPointClick={(id) => console.log(id)}
/>
);
}
function DashboardFixed({ region }) {
const filters = React.useMemo(() => ({ region, includeArchived: false }), [region]);
const onPointClick = React.useCallback((id) => console.log(id), []);
return <Chart filters={filters} onPointClick={onPointClick} />;
}
Common misconception | Correct framing |
|---|---|
“React re-renders the whole app.” | React schedules work for the affected part of the tree. But parent renders do propagate unless you add bailout boundaries (memo) and keep identities stable. |
“Virtual DOM diff always computes the minimal edit.” | Reconciliation is heuristic and optimized for common UI patterns. With good keys/types, it’s very efficient; with bad keys/identity churn, it can do extra work. |
“If a component re-rendered, the DOM changed.” | Not necessarily. If the computed output is the same (or the changed parts don’t affect the DOM), commit can be a no-op. |
“useCallback/useMemo always improves performance.” | They help only when they prevent real work (rerenders of expensive children, expensive computations). Overuse adds complexity and overhead. |
Optimization lever | What it actually reduces | Best use |
|---|---|---|
Colocate state (move it down) | The number of components affected by updates | Keep state as close as possible to where it’s used, so fewer parents re-render. |
Split components | The size of the rerendered subtree | Separate “fast-changing” parts from “mostly-static” parts. |
React.memo / PureComponent | Re-rendering of child components | Pure/presentational children with stable props. |
Stable prop identities | False-positive “prop changed” signals | Avoid inline objects/functions when they’re passed to memoized children. |
Memoize Context value | Broad consumer rerenders | Wrap provider value creation in |
Another classic production bug: context fanout
A provider value object created inline changes identity every render. That means every consumer can re-render even if only one tiny field in the object changed. This is why rerender debugging often starts by looking at provider values, not only at prop comparisons.
const SearchContext = React.createContext(null);
function App() {
const [theme, setTheme] = React.useState('light');
const [query, setQuery] = React.useState('');
// ❌ New object every render -> all consumers re-render
// const value = { theme, query, setQuery };
// ✅ Stable when only query changes? Split contexts or memoize
const value = React.useMemo(() => ({ theme, query, setQuery }), [theme, query]);
return <SearchContext.Provider value={value}><Layout /></SearchContext.Provider>;
}
Two important gotchas
• StrictMode (dev): React may intentionally run render logic more than once to surface unsafe side effects. Don’t treat “double logs” in dev as production behavior.
• Keys/type changes: Changing a component’s key or swapping wrappers (e.g., Fragment ↔ div) can cause a remount (state reset), not just a re-render.
function Editor({ userId }) {
const [draft, setDraft] = React.useState('');
return <input value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
// ❌ Re-render keeps local state, but changing key remounts and wipes it
<Editor userId={activeUserId} key={activeUserId} />
Commit vs rerender vs remount
A rerender is compute work. A commit is visible DOM work. A remount is identity loss plus fresh local state. Keeping those separate makes render debugging much easier.
React re-renders when state/props/context might have changed. A re-render is “recompute the next UI tree”, not “mutate the DOM”. React then reconciles and commits only necessary DOM changes. Unnecessary re-renders usually come from unclear state ownership, unstable prop identities (inline objects/functions), broad context updates, or bad keys.
Use this as one explanation rep, then continue with the React interview questions cluster or a guided prep path.