React Interview Questions: Re-render Causes and Performance
Reviewed by Mark Dickie · Last updated
React re-renders are the process by which React re-executes a component's function body to produce a new virtual DOM tree, and understanding when and why they happen is central to any React performance interview. The four triggers to know cold are: a component's own state change, a parent re-rendering, a context value change, and a prop change from a parent. Excess re-renders slow down large apps, so interviewers test whether you can identify the cause and apply the right fix: React.memo, useMemo, useCallback, or restructuring the component tree.
What causes a React component to re-render?
Every React component re-renders when its own state changes via the useState or useReducer setter. A component also re-renders when its parent re-renders, unless it is wrapped in React.memo with referentially stable props. Context value changes re-render every consumer of that context, regardless of React.memo on the consuming component.
| Trigger | Re-renders the component? | How to prevent |
|---|---|---|
| Component's own state change | Yes, always | Expected; let it happen |
| Parent re-renders | Yes, by default | Wrap child in React.memo + stable props |
| Context value changes | Yes, all consumers | Split context or memoize the value |
| Props change (new reference) | Yes | Stabilize with useMemo / useCallback |
How do you prevent unnecessary re-renders in React?
- Wrap child components in
React.memoso they skip re-render when props are shallowly equal. - Use
useMemoto keep object and array props referentially stable across parent re-renders. - Use
useCallbackto keep function props stable so memoized children don't re-render on every parent render. - Move state down to the smallest component that needs it, so siblings don't re-render when that state changes.
- Split context providers so a value change in one provider doesn't force unrelated consumers to re-render.
Key facts
- Tarmac's React interview questions cover 18 questions at difficulty 2–5 of 5.
- Tarmac tracked 2,587 job postings asking for React in August 2026.
- Roles asking for React advertise a median base salary of US$175,000, across 568 job postings as of August 2026.
- Tarmac last reviewed these React interview questions on 31 August 2026.
At a glance
| Questions | 18 |
|---|---|
| Difficulty | 2–5 of 5 |
| Formats | Multiple choice, True / false, Multiple answer, Short answer, Find the bug, Code output, Flashcard |
What you'll review
- rerender causes
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
A page component holds const [searchTerm, setSearchTerm] = useState('') and renders both <SearchInput value={searchTerm} onChange={setSearchTerm} /> and a completely unrelated <ExpensiveDashboardChart data={chartData} /> as siblings. Every keystroke in the search box re-renders ExpensiveDashboardChart too, even though none of its own props changed. What's the most direct fix?#
Options
Show answer
Move searchTerm state down into SearchInput itself, so keystroke updates only re-render that component. ExpensiveDashboardChart re-renders solely because it shares a parent with the changing state — React re-renders a component's whole subtree by default whenever that component re-renders, siblings included, regardless of whether a given sibling's own props changed. Since searchTerm is only read and written by the input, colocating it there confines the re-render to the input's small subtree and leaves the page, and the chart, untouched.
The chart re-renders only because it happens to share a parent with the state that's changing — React re-renders a component's entire subtree by default whenever that component re-renders, siblings included, regardless of whether a given sibling's own props changed. searchTerm doesn't need to live at the page level at all; it's only ever read and written by the input, so colocating it there confines the re-render to the input's own small subtree and leaves the page (and the chart) untouched. b is a category error — useMemo memoizes a computed value inside a component, not a component itself, and does nothing to stop a parent-driven re-render; that's what React.memo is for, and even then it would only treat the symptom. c mischaracterizes key, which is an identity hint for reconciliation among siblings in a list, not a re-render-isolation mechanism. d is the trap this question tests: sibling re-renders are the default, but they follow from where state is placed and can be designed around, not an unavoidable law.
By default, when a parent component re-renders, React skips re-rendering a child whose own props haven't changed, unless that child is wrapped in React.memo.#
Options
Show answer
False. By default React re-renders a component's entire subtree whenever that component re-renders, including every child, regardless of whether a given child's own props changed — nothing is skipped automatically. React.memo exists precisely because this isn't the default behavior: it's an explicit opt-in a component author adds so React compares that component's props and can potentially skip re-rendering it.
The opposite is true by default: React re-renders a component's entire subtree whenever that component re-renders, including every child, regardless of whether a given child's props changed. Nothing is skipped automatically. React.memo exists precisely because this isn't the default — it's an explicit opt-in a component author adds when they want React to compare that component's props and potentially skip re-rendering it.
Which of these cause a React component to re-render?#
Options
Pick every one that applies.
Show answer
Three things trigger a re-render: calling the component's state setter with a new value, receiving new props, and its parent re-rendering (unless the component is memoized). Mutating a plain, non-state variable does not — React never observes that change, so it schedules no render. Only state, props, and parent renders flow through React's update mechanism.
A component re-renders when its state changes, its props change, or its parent re-renders (unless memoized). Mutating a normal variable does not schedule a render — React never sees the change.
Calling a state setter with a value that is Object.is-equal to the current state always triggers a re-render.#
Options
Show answer
False. When the new state is Object.is-equal to the current state, React bails out — it may run the component once more before bailing, but it won't re-render children or commit any changes. Setting the same primitive value is effectively a no-op, so it does not always trigger a visible re-render.
React bails out when the new state is Object.is-equal to the current state — it may still re-render the component once before bailing, but it will not re-render children or commit changes. Setting the same primitive value is effectively a no-op.
What triggers a React function component to re-render?#
Show answer
A component re-renders when its own state changes (a setter is called with a different value), when it receives new props, when a context value it consumes changes, or when its parent re-renders. Mutating a non-state variable does not trigger a re-render because React is unaware of the change.
The triggers are state change, new props, a consumed context value changing, or a parent rendering (unless the child is memoized and its props are unchanged). Plain variable mutation is invisible to React.
const SubmitButton = React.memo(function SubmitButton({ label, onClick }) { return <button onClick={onClick}>{label}</button>; }); function Form() { const [text, setText] = useState(''); return ( <> <input value={text} onChange={(e) => setText(e.target.value)} /> <SubmitButton label="Save" onClick={() => save(text)} /> </> ); } Every keystroke in the input causes SubmitButton to re-render too, even though label never changes and SubmitButton is wrapped in React.memo. Why?#
Options
Show answer
onClick={() => save(text)} allocates a brand-new function object on every render of Form, and React.memo's shallow comparison checks each prop with Object.is, which reports two distinct function objects as different even when they'd behave the same. So onClick reads as changed on every render and memo re-renders SubmitButton regardless of label. Wrapping the handler in useCallback (with stable dependencies) gives it a stable reference so memo can actually skip work when nothing meaningful changed.
Defining () => save(text) inline means every render of Form produces a syntactically new function object. React.memo's shallow comparison checks each prop with Object.is, and Object.is reports two distinct function objects as different even if they'd behave identically — so onClick always reads as "changed," and memo re-renders SubmitButton on every Form render regardless of label. Wrapping the handler in useCallback(() => save(text), [text]) (stable whenever text hasn't changed) would let memo actually skip work. b is wrong — memo compares any prop type with Object.is, not just primitives; it's just that objects and functions rarely stay reference-equal across renders. c is wrong — "Save" is a string primitive, and Object.is compares primitives by value, so it's equal across renders. d is a red herring: memo behaves identically regardless of function-declaration syntax.
Chart is wrapped in React.memo and data never changes here. Switching tabs still re-renders Chart every time. Which line is the source of the bug?#
const Chart = React.memo(function Chart({ data, colors }) {
return <SvgChart data={data} colors={colors} />;
});
function Dashboard({ data }) {
const [tab, setTab] = useState('overview');
return (
<div>
<TabBar tab={tab} onChange={setTab} />
<Chart data={data} colors={['#4f46e5', '#16a34a', '#dc2626']} />
</div>
);
}Show answer
The bug is on line 10.
colors={['#4f46e5', '#16a34a', '#dc2626']} allocates a brand-new array literal on every render of Dashboard, even though its contents never change. React.memo's shallow comparison checks colors with Object.is, and two distinct array objects are never Object.is-equal no matter what they contain, so Chart's props always read as changed and memo re-renders it on every Dashboard render — including every tab switch, which has nothing to do with data or colors. Fix: hoist the array to a module-level constant (created once, outside the component) or wrap it in useMemo(() => [...], []) so the same reference is reused across renders.
Row is wrapped in React.memo and its only prop is label:#
Options
Show answer
Row re-renders regardless of React.memo, because memo's shallow prop comparison only gates re-renders driven by the parent passing new props — it has no visibility into a context value a component reads for itself via useContext. React's own docs are explicit that a memoized component still re-renders when a context it's using changes; context reads sit entirely outside what memo compares. Every consumer of ThemeContext, memoized or not, re-renders on every provider value change, whether or not label changed.
React.memo's shallow comparison only inspects the props passed down by the parent; it says nothing about a context value a component subscribes to internally via useContext. React's own docs state this directly: a memoized component "will still re-render when a context that it's using changes" — memoization only concerns props from the parent. So every consumer of ThemeContext, memoized or not, re-renders when the provider's value changes, regardless of whether label changed. b and c invent a comparison React doesn't perform — context reads are never folded into the props shallow-equality check — and d invents a "resync once, then block" behavior that doesn't exist; context changes are never throttled by memo.
A component Child is wrapped in React.memo. Which of these can still cause Child to re-render, even though its parent passes it the exact same prop values every time? Select all that apply.#
Options
Pick every one that applies.
Show answer
Three things still force Child to re-render despite React.memo: its own useState/useReducer updates, a useContext subscription whose provider value changes, and a useSyncExternalStore subscription whose store emits a new snapshot. React.memo only gates one thing — whether a parent re-rendering with shallowly-equal props should force a re-render — and has no visibility into re-renders Child triggers on itself or through a data source it subscribes to directly. The same parent passing identical primitive props is exactly the case memo is built to skip, and a sibling's own state change never reaches an unrelated sibling's subtree at all.
React.memo only gates one thing: whether a parent re-rendering with unchanged (shallowly-equal) props should force Child to re-render too. It has nothing to say about re-renders Child triggers on itself — its own useState/useReducer updates — or re-renders driven by data sources Child subscribes to directly, like context or an external store via useSyncExternalStore; all of those re-render Child regardless of what memo decided about its props. Passing Child the exact same primitive prop values as before is exactly the case memo is built to skip: same props, shallowly equal, so no re-render. A sibling re-rendering because its own state changed doesn't reach Child at all — a sibling's own state update re-renders that sibling's own subtree, not an unrelated sibling under the same parent.
A SettingsContext.Provider wraps a large part of the tree and supplies { theme, locale, currentUser }. Ten descendant components call useContext(SettingsContext), but each only destructures the one field it cares about (e.g. const { theme } = useContext(SettingsContext)). Which statements are true about what happens when currentUser changes and a new context value is provided? Select all that apply.#
Options
Pick every one that applies.
Show answer
Three things are true: all ten consumers re-render, including ones that never read currentUser; React compares the new context value to the old one as one value via Object.is, so it has no concept of which field a given consumer actually reads, only whether the value reference changed; and splitting the context into separate ThemeContext/LocaleContext/CurrentUserContext providers would let a component subscribed only to ThemeContext skip this re-render. React.memo does not help here — these re-renders come directly from useContext, not from a parent passing new props, and memo has no effect on that path.
Context has no concept of "the parts of the value a given consumer reads" — useContext subscribes to the whole value, and React re-renders every subscribed consumer whenever that value is a new reference (as determined by Object.is), regardless of which fields changed or which fields a particular consumer destructures. The standard mitigation is exactly what c describes — splitting one broad context into several narrower ones so a component only re-renders when the specific context it actually subscribes to changes. d is false and a common misconception: React.memo has no effect here, because these re-renders are triggered directly by useContext, not by a parent passing new props, which is the only path memo gates. e is false for the same reason as a/b: the comparison is on the value as a whole, so field-level selectivity isn't something plain context provides.
In a reducer, an unhandled action falls through to default: return state;, returning the exact same object reference that was passed in. Dispatching that action still lets React bail out of re-rendering, exactly the way calling a useState setter with an Object.is-equal value does.#
Options
Show answer
True. useReducer's dispatch checks the reducer's return value against the current state using the exact same Object.is comparison useState's setter uses. Returning the literal same reference — unmodified, not a spread or a copy — is Object.is-equal to the current state, so React bails out of the re-render. This bail-out only works because the object is returned unchanged; a reducer branch that spreads (return { ...state }) produces a new reference and defeats the bail-out even when every field's value is identical.
useReducer's dispatch checks the reducer's return value against the current state with the same Object.is comparison useState's setter uses. Returning the literal same reference — unmodified, not a copy or a spread — is Object.is-equal to the current state, so React bails out and does not schedule a re-render for that dispatch. This only works because the state is returned unchanged; a branch that spreads (return { ...state }) or copies and returns a new object produces a different reference and defeats the bail-out even when every field's value is identical.
Starting from mount, the user clicks "bump other" three times and nothing else (count stays 0 throughout). In total, how many times does Child's function body run?#
let childRenders = 0;
const Child = React.memo(function Child({ count, onClick }) {
childRenders += 1;
return <button onClick={onClick}>{count}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
const onClick = () => setCount(count + 1);
return (
<div>
<button onClick={() => setOther(other + 1)}>bump other</button>
<Child count={count} onClick={onClick} />
</div>
);
}Options
Show answer
4 — one mount plus one re-render per click, because `onClick` is a new function reference every time Parent renders
Each click on "bump other" updates other, re-rendering Parent, which redefines onClick as a brand-new arrow function every time. React.memo compares every prop with Object.is, and onClick is a different function reference on each Parent render even though count stays 0, so the shallow comparison finds a changed prop and Child re-renders. That's 1 mount + 3 re-renders (one per click) = 4 total. If onClick were wrapped in useCallback(() => setCount(count + 1), [count]) (stable across renders where count doesn't change), the three clicks would produce zero additional Child renders, since both count and onClick would be Object.is-equal to their previous values.
Starting from mount, the user clicks "tick" three times (count is never changed, only tick is). In total, how many times does CountDisplay's function body run?#
let displayRenders = 0;
const CountContext = createContext({ count: 0 });
function CountDisplay() {
const { count } = useContext(CountContext);
displayRenders += 1;
return <span>{count}</span>;
}
function App() {
const [count, setCount] = useState(0);
const [tick, setTick] = useState(0);
return (
<CountContext.Provider value={{ count }}>
<button onClick={() => setTick(tick + 1)}>tick</button>
<CountDisplay />
</CountContext.Provider>
);
}Options
Show answer
4 — one mount plus one re-render per tick, because `value={{ count }}` creates a new object every render of App, and useContext compares the whole value by reference
Each tick updates App's tick state, re-rendering App, which evaluates value={{ count }} fresh — a brand-new object every render, even though count's own value (0) never changes. useContext compares the previous context value to the new one as a whole via Object.is, and two different object references are never equal no matter what fields they contain, so CountDisplay re-renders on every App render regardless of whether count itself changed. That's 1 mount + 3 re-renders = 4. Wrapping the provider value in useMemo(() => ({ count }), [count]) would give it a stable reference across renders where count doesn't change, and the three ticks would produce zero additional CountDisplay renders.
Every consumer of AuthContext re-renders on every render of AuthProvider, even on renders where user, and what login/logout do, are functionally unchanged. Which line causes this?#
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (u) => setUser(u);
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}Show answer
The bug is on line 8.
value={{ user, login, logout }} builds a new object literal on every render of AuthProvider, so the context value has a different reference every time — even on renders where user and the behavior of login/logout haven't meaningfully changed. Because useContext compares the previous and next context values with Object.is, a changed reference is all it takes: every consumer re-renders on every AuthProvider render, regardless of which field it actually reads. login and logout are also redefined as new function objects every render, compounding the problem for any consumer that only cares about calling them. Fix: memoize the value with useMemo(() => ({ user, login, logout }), [user, login, logout]), and wrap login/logout in useCallback so their references stay stable too — then the value object's reference only changes when one of its real inputs changes.
The user clicks Edit on the second todo, putting it into edit mode. They then delete the first todo. The row that was third (now second) unexpectedly opens in edit mode too, even though its own Edit button was never clicked. Which line is the source of the bug?#
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoRow key={index} todo={todo} onDelete={() => onDelete(todo.id)} />
))}
</ul>
);
}
function TodoRow({ todo, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
return (
<li>
{isEditing ? <EditForm todo={todo} /> : <span>{todo.text}</span>}
<button onClick={() => setIsEditing(true)}>Edit</button>
<button onClick={onDelete}>Delete</button>
</li>
);
}Show answer
The bug is on line 5.
key={index} ties each TodoRow's identity to its position, not to which todo it represents. Deleting the first todo shifts every remaining todo down one index, so the component instance that used to render at index 1 (currently in edit mode) is now matched against the todo that used to be at index 2, because both are "key=1" as far as React can tell across renders — same key means React reuses the same component instance and its state (isEditing: true) in place, only updating its data. The result: the third todo inherits the second todo's edit state, while the second todo's own edit state is lost (it inherits whatever index 0's instance had). Fix: key={todo.id}, so identity follows the data itself rather than its position, and a delete or reorder can never cross state between items.
You wrap a component in React.memo, but it keeps re-rendering every time an unrelated part of the app updates — tracing it down, the component reads a value from context (or from an external store via useSyncExternalStore). Why doesn't React.memo prevent this, and what does memo actually guard against?#
Show answer
React.memo only compares the props a component's parent passes it and skips a re-render when those props are shallowly equal to last time — that's the entire mechanism. It has no knowledge of, and no effect on, re-renders the component triggers for itself by subscribing to something outside the parent-child prop chain: its own useState/useReducer, a useContext subscription whose provider value changed, or a useSyncExternalStore subscription whose store emitted a new snapshot. Those re-renders bypass the parent-driven path entirely, so memo's props check never even runs for them. To stop context-driven re-renders, the fix has to target the context itself — e.g. splitting a broad context into narrower ones, or memoizing the provider's value — not the memo wrapper on the consumer.
This is one of the most commonly misunderstood boundaries of React.memo in interviews: it's a props-in comparison, not a general 're-render blocker.' It gates exactly one re-render trigger — a parent re-rendering with the same shallow props — and has zero effect on the other triggers (own state, context, external stores) because those don't go through the parent-props path memo inspects at all.
Do useState and useReducer bail out of a re-render the same way when the new state is unchanged?#
Show answer
Yes and no. Both apply the identical Object.is bail-out: if the value handed to the setter (or returned by the reducer) is Object.is-equal to the current state, React skips the re-render. The difference is behavioral, not mechanical — useState is commonly called with a primitive or the same object reference, so the bail-out fires often; useReducer's reducers conventionally spread state ({ ...state, ... }) to build the next value, which allocates a new object reference on every dispatch, so the bail-out essentially never fires unless a branch explicitly returns the exact same state object unmodified (e.g. for an unhandled action).
This distinction trips people up because it sounds like two different mechanisms when it's really one mechanism applied to two different calling conventions. Interviewers use it to probe whether a candidate actually understands Object.is-by-reference versus deep equality, not just the surface behavior.
One version of a component uses const [state, setState] = useState({ count: 0 }); another uses const [state, dispatch] = useReducer(reducer, { count: 0 }) where the reducer has case 'noop': return { ...state };. Calling setState(state) — passing back the exact same object — does not cause a re-render. Dispatching { type: 'noop' } does cause a re-render. Why the difference?#
Options
Show answer
useState and useReducer apply the exact same Object.is bail-out to the new state versus the current one — it isn't two different rules. The difference here is what each call site actually produces: setState(state) hands back the literal same object, and Object.is(obj, obj) is true, so React bails out. The reducer's { ...state } spread builds a brand-new object every dispatch, and even with identical field values, Object.is on two distinct object references is always false, so the re-render proceeds. In practice this means a reducer that always spreads state never gets a free bail-out for a no-op action — only an unmodified return state; does.
React applies the exact same Object.is bail-out to both hooks — "if the new value is Object.is-equal to the current state, skip the re-render" is one rule, not two different ones for the two hooks. What differs here is what the two call sites hand React. setState(state) passes the literal same object back, and Object.is(obj, obj) is true, so it bails out. The reducer's { ...state } spread constructs a brand-new object every dispatch, and even though its fields are equal, Object.is on two distinct object references is always false, so React proceeds with the re-render. This is a real production trap: a reducer that always spreads never gets a free bail-out for a no-op action, even when nothing logically changed — only an unmodified return state; does. b is false (useReducer does bail out — the docs cover it explicitly); c misdescribes what's compared (the resulting state, not action.type); d is wrong — useState's bail-out applies to any value type via Object.is, object or primitive, not just primitives.
Sources
The official documentation these questions are checked against:
Related interview questions
Job market
See react salaries and hiring demand from live job postings.
Practise these until they stick
That's every question we hold on this topic, and the page marks what you pick. What it can't do is remember. A free account keeps every answer, and what you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan