React useState interview questions
Reviewed by Mark Dickie · Last updated
8 questions on this page you can answer and see marked on the spot. Go to the first one
React useState is a hook that lets a function component hold and update local state across renders, returning an array of the current value and a setter function. The setter accepts either a replacement value or an updater callback (prev) => next, and React schedules the re-render asynchronously rather than mutating state in place.
For an interview, expect questions on three things: the difference between passing a value versus a callback to the setter, how React batches multiple updates inside the same event handler, and why calling useState conditionally or in a loop breaks the rules of hooks. You should also be able to explain lazy initialization with the useState(() => initialValue) form, which matters when the initial value is expensive to compute.
| Concept | What an interviewer checks |
|---|---|
| Setter callback form | Do you know (prev) => next avoids stale-state bugs in rapid updates? |
| Batching | Can you explain that multiple setState calls in one handler produce one re-render? |
| Lazy init | Do you pass a function when the initial state is costly to build? |
| Functional vs direct update | Can you spot when a direct value is safe and when the callback is required? |
| Rules of hooks | Can you explain why useState must run in the same order every render? |
What does a React useState interview typically test?
- Identifying stale closures in event handlers and fixing them with the updater callback.
- Explaining why object state requires you to spread the previous value rather than replacing it wholesale.
- Predicting how many times a component renders after several
setStatecalls in the same handler. - Recognizing when
useStateshould be replaced withuseReduceror lifted to a parent. - Debugging code that calls
useStateinside a condition, loop, or early return.
When should you use lazy initialization in useState?
Pass a function to useState when the initial value comes from an expensive calculation, like parsing a large JSON string or reading from localStorage. React runs that function only on the first mount. If you pass the expression directly, React computes it on every render even though the result is discarded after the first one. The lazy form is useState(() => computeExpensiveInitial()), and it keeps subsequent renders cheap.
The quiz below puts these concepts into practice with real interview questions drawn from common React screening rounds.
Key facts
- Tarmac's React interview questions cover 25 questions at difficulty 1–5 of 5.
- Tarmac tracked 2,154 job postings asking for React in September 2026.
- Roles asking for React advertise a median base salary of US$175,250, across 692 job postings as of September 2026.
- Tarmac last reviewed these React interview questions on 13 October 2026.
At a glance
| Questions | 25 |
|---|---|
| Difficulty | 1–5 of 5 |
| Formats | True / false, Fill in the blank, Short answer, Code output, Multiple choice, Flashcard, Ordering, Find the bug |
What you'll review
- usestate
- rerender causes
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
In a React function component, calling the setter returned by useState (for example, setCount(5)) synchronously updates the count variable on the very next line of the same function. True or false?#
Options
Show answer
False. Calling a useState setter does not update the state variable on the next line of code. React queues the update and schedules a re-render, so the local variable keeps its previous value until the component renders again and useState returns the new state.
The statement is false. A useState setter does not change the local count variable in the currently executing render. React queues the update and schedules a re-render, so count keeps its old value on the next line. The new value only appears when the component re-renders and useState returns it on that later call.
const [count, _____] = _____(0) declares a piece of state initialised to 0 along with its setter.#
Show answer
const [count, **setCount**] = **useState**(0) declares a piece of state initialised to 0 along with its setter.
useState(0) returns a [value, setter] pair; destructuring names them count and setCount by convention. Calling setCount schedules a re-render with the new value.
When you call useState(initialValue) in a React function component, what does the hook return?#
Show answer
useState returns a two-element array: [0] is the current state value (initially initialValue) and [1] is the setter function used to update that state. By convention it is destructured as const [state, setState] = useState(initialValue). The setter does NOT merge objects the way this.setState does in class components.
useState returns a two-element array whose first element is the current state value (initialized to initialValue on the first render) and whose second element is the setter function used to update that state. By convention we destructure it as const [state, setState] = useState(initialValue). It is not an object, a single value, or a Promise.
In React, useState returns a two-element array: the current state value and a setter function that updates it. By widely-followed convention, the setter is named by prefixing the state variable name with set. Given const [count, _____] = useState(0);, what conventional name fills the blank?#
Show answer
In React, useState returns a two-element array: the current state value and a setter function that updates it. By widely-followed convention, the setter is named by prefixing the state variable name with set. Given const [count, **setCount**] = useState(0);, what conventional name fills the blank?
React convention names the setter returned by useState by prefixing the state variable name with set, so for a state variable named count the conventional setter name is setCount. Any valid JavaScript identifier would work at runtime, but setCount is the widely-followed community convention.
In a React function component, you see this code. On the first render, what text is displayed inside the <p> element?#
function Counter() {
const [count, setCount] = useState(0);
return <p>{count}</p>;
}Show answer
0
useState(0) initializes the state variable count to 0 on the first render. The JSX <p>{count}</p> therefore renders the number 0 as the text content of the paragraph, so the displayed value is 0.
What does the React useState hook return?#
Show answer
useState returns a two-element array: the first element is the current state value, and the second element is a function that updates that state. It is conventionally destructured, e.g. const [count, setCount] = useState(0).
useState returns a two-element array. The first element is the current state value, and the second element is a function (the state setter) that updates that state and triggers a re-render. This is why it is conventionally destructured, as in const [count, setCount] = useState(0).
The initial value passed to useState(initialValue) is used only on the first render and ignored on all subsequent renders.#
Options
Show answer
True
On the initial render, useState uses the provided initial value (or calls the initializer function). On every subsequent render, useState ignores the argument entirely and returns the current state value instead.
Given this React component, the button is clicked exactly once. What number is displayed inside the button after the click is processed and React has re-rendered?#
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}}
>
{count}
</button>
);
}Show answer
1
All three setCount calls run inside the same click handler, and each reads the count value from that render's closure, which is 0. Each call therefore enqueues the same update, setCount(0 + 1), i.e. setCount(1). React batches these updates within the event, so the final state is 1 and the button re-renders showing 1, not 3.
What is the correct way to increment state that depends on its previous value?#
Options
Show answer
Use the updater form: setCount(c => c + 1). React passes the latest pending state into the function, so it stays correct across batching and avoids stale-closure bugs. Reading the count variable directly — setCount(count + 1) — captures the value from the current render, so calling it twice still increments only once. Whenever the next state depends on the previous, pass a function, not a value.
The updater form setCount(c => c + 1) receives the latest pending state, so it is safe across batching and stale closures. Reading the count variable can use a stale value.
Clicking the button in this React component does not change the displayed age. Explain why, and describe the fix.#
Show answer
The handler mutates the existing state object (user.age += 1) and then passes that same object reference to setUser. React compares the new state with the previous state by identity (Object.is), finds no change, and does not re-render with new output, so the displayed age stays at 36. The fix is to leave the state object untouched and pass a new object, for example setUser(prev => ({ ...prev, age: prev.age + 1 })), so React sees a different reference and re-renders.
The handler changes the age property of the existing state object in place and then calls setUser with that same object. React uses an identity check (Object.is) to decide whether state changed; since the reference is unchanged, it treats the update as a no-op and the rendered age does not update. The fix is to create a new object, such as setUser(prev => ({ ...prev, age: prev.age + 1 })), so the reference differs and React re-renders with the new value.
When updating React state based on its previous value, pass an updater _____ to the setter so React computes the next state from the most recent state it has queued, rather than from a value captured in the current render's closure.#
Show answer
When updating React state based on its previous value, pass an updater function to the setter so React computes the next state from the most recent state it has queued, rather than from a value captured in the current render's closure.
A React state setter accepts either a new value or an updater function. When the next state depends on the previous state, passing an updater function makes React call it with the latest queued state value. Passing a computed value instead uses whatever state was captured in the current render's closure, which can be stale when several updates are batched in one event handler.
Immediately after calling setCount(count + 1), what does reading count on the next line return (within the same event handler)?#
Options
Show answer
The old value. State setters schedule a re-render; they don't mutate the existing count binding. Within the same render and event handler, count keeps the value it had when that render ran, and the updated value only appears on the next render. This is why sequential updates that build on each other need the updater form setCount(c => c + 1) rather than reading count again.
State setters schedule a re-render; they do not mutate the existing count binding. Within the same render/handler count keeps its current value, and the updated value is only visible in the next render. This is why updater functions are needed for sequential updates.
When should you pass a function to useState, e.g. useState(() => expensiveInit())?#
Show answer
Use the lazy initializer form when computing the initial state is expensive. The function runs only on the first render; passing the value directly (useState(expensiveInit())) would recompute it on every render even though it's ignored after mount.
useState(init) evaluates init every render but only uses it once; useState(() => init) defers the work to mount. Both yield the same initial state.
A user types one character into a controlled <input value={text} onChange={…} />. Order what happens.#
Put these in order
Show answer
A controlled input's value comes from state, so one keystroke flows through four ordered steps:
- The user presses a key.
onChangecallssetText(newValue).- React re-renders the component.
- The input displays the new
value.
Without the setText step the input would freeze at its old value, because the displayed value is driven by state, not by the DOM.
A controlled input's displayed value comes from state, so the keystroke fires onChange, which sets state, which triggers a re-render that feeds the new value back into the input. Skipping setState would leave the input frozen at its old value.
Consider the following React component (React 18, rendered normally with StrictMode off). The user clicks the button exactly once, starting from the initial mount. What number is rendered inside the <button> after the click? Trace the state updates carefully, noting the difference between the direct-value form and the functional-updater form of setN.#
import { useState } from 'react';
function Counter() {
const [n, setN] = useState(0);
function handleClick() {
setN(n + 1);
setN(n + 1);
setN(prev => prev + 1);
}
return <button onClick={handleClick}>{n}</button>;
}Show answer
2
On the initial render n is 0, and the closure in handleClick captures that value. The two direct calls setN(n + 1) both compute 0 + 1 = 1, so they queue the value 1 twice; they do not see each other's results because they read the stale n from the closure. The third call passes an updater function, which React applies to the pending queue in order: it receives the latest pending value 1 as prev and returns 1 + 1 = 2. React batches all three updates within the same event handler into a single re-render, so the button shows 2.
Assume React 18 with automatic batching. The Counter component below is rendered once, and its button is then clicked exactly two times. What text does the button display after the second click? Give the exact text content of the button.#
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [double, setDouble] = useState(() => count * 2);
return (
<button onClick={() => {
setCount(c => c + 1);
setCount(c => c + 1);
setDouble(d => d + count);
}}>
{count}-{double}
</button>
);
}Show answer
4-2
The lazy initializer () => count * 2 runs only on mount, when count is 0, so double starts at 0. Each click queues two functional setCount updaters that chain on the previous value, so count rises by 2 per click. The setDouble(d => d + count) call reads count from the render's closure, which is the value rendered before that click: 0 on the first click and 2 on the second. After click 1, count is 2 and double is 0 + 0 = 0. After click 2, count is 4 and double is 0 + 2 = 2. The button therefore displays 4-2.
The ProfileForm component below has a bug: typing into one input field erases the values of all other fields. Identify the buggy line (1-based line numbers).#
import { useState } from 'react';
export function ProfileForm() {
const [form, setForm] = useState({ name: '', email: '', bio: '' });
function handleChange(field, value) {
setForm({ [field]: value });
}
return (
<form>
<input value={form.name} onChange={e => handleChange('name', e.target.value)} />
<input value={form.email} onChange={e => handleChange('email', e.target.value)} />
<textarea value={form.bio} onChange={e => handleChange('bio', e.target.value)} />
</form>
);
}Show answer
The bug is on line 7.
A useState setter replaces the state wholesale; it does not shallow-merge the way the old class setState did. Line 7 calls setForm with a new single-key object { [field]: value }, so form becomes an object that contains only the field just typed into, and every other field is discarded (their inputs then render as undefined/empty). The fix is to spread the previous state, for example setForm(prev => ({ ...prev, [field]: value })), so the other keys are carried forward.
Starting at 0, what is count after a single click?#
function Counter() {
const [count, setCount] = useState(0);
const onClick = () => {
setCount(count + 1);
setCount(count + 1);
};
return <button onClick={onClick}>{count}</button>;
}Options
Show answer
1
Both calls close over the same render's count (0), so each computes 0 + 1 and the state ends at 1. The updater form setCount(c => c + 1) would reach 2.
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.
A React 18 component renders a button whose label is the current count state, which starts at 0. The user clicks the button exactly once, and the click handler below runs. After React finishes processing that single click, what number is displayed inside the button? Give the number only.#
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>;
}Show answer
1
All four setCount calls in the handler are queued and batched into one re-render. The closure's count is 0 for the whole handler, so both direct-value calls compute 0 + 1 = 1. Processing the queue in order from the base state 0: the first call sets the value to 1; the first functional update receives prev = 1 and returns 2; the second functional update receives prev = 2 and returns 3; the final direct-value call sets the value to the stale 0 + 1 = 1, overwriting the earlier functional-update work. The rendered button therefore shows 1, not 3 or 4.
The counter component below should increase its displayed value by 1 every second, but the value never advances past 1. Which line contains the bug? Select the line number.#
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <p>{count}</p>;
}Show answer
The bug is on line 8.
The effect has an empty dependency array, so it runs only once on mount, and the interval callback closes over the count value from that first render, where count is 0. Line 8, setCount(count + 1), therefore always computes 0 + 1 and sets the state to 1 on every tick. Re-renders do not re-create the interval because the effect does not re-run, so the stale closure persists and the display stays at 1. The fix is the functional updater setCount(prev => prev + 1), which receives the latest committed state from React rather than a captured variable.
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.
Consider this React 18 component, rendered normally (not inside React.StrictMode). Its setInterval fires exactly 5 times, and each tick's render finishes before the next tick. Each console.log call prints one value on its own line. Give the complete sequence of values printed, including the initial mount render, separated by commas, in the order they appear.#
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
setMultiplier(m => m + 1);
}, 1000);
return () => clearInterval(id);
}, []);
console.log(count * multiplier);
return null;
}Show answer
0, 2, 3, 4, 5, 6
The effect has an empty dependency array, so the interval callback closes over the initial count value of 0 for the component's lifetime. Each tick therefore calls setCount(0 + 1), i.e. setCount(1). On the first tick count becomes 1, while multiplier (updated with a functional updater that always sees the latest state) becomes 2; React batches these into one render, which prints 1 × 2 = 2. On every later tick setCount(1) leaves count at 1, but setMultiplier(m => m + 1) still changes the multiplier to 3, 4, 5, 6, so the component re-renders each time and prints 3, 4, 5, 6. The mount render prints 0 × 1 = 0. The output is therefore 0, 2, 3, 4, 5, 6. Whether React takes its eager-bailout path for the redundant count update does not change the printed values, because the multiplier change forces a render on every tick.
Consider the following React component. What appears in the browser console after the initial render and exactly one click of the button? Assume React 18 with automatic batching. Enter the exact console output, one log per line.#
import React, { useState, useEffect } from 'react';␊␊function App() {␊ const [count, setCount] = useState(0);␊␊ useEffect(() => {␊ console.log('render:', count);␊ }, [count]);␊␊ const handleClick = () => {␊ setCount(count + 1);␊ setCount(count + 1);␊ setCount((prev) => prev + 1);␊ };␊␊ return <button onClick={handleClick}>Click</button>;␊}␊␊// Simulate: initial render, then exactly one click of the button.Show answer
render: 0
render: 2
On initial render, count is 0, so the useEffect logs render: 0. The click handler's closure captures count = 0 from this render's scope. The first setCount(count + 1) enqueues a replace update with value 1 (0 + 1). The second setCount(count + 1) enqueues another replace update, also with value 1 — the same stale 0, not the intermediate 1, because the variable count is not reassigned within the handler. The third setCount((prev) => prev + 1) enqueues a functional update that React applies to the latest pending state. Processing the batch in order: replace → state = 1, replace → state = 1, functional → state = 1 + 1 = 2. One re-render occurs with count = 2. The useEffect fires (count changed 0 → 2) and logs render: 2. Total output is two lines: render: 0 then render: 2.
The PollingComponent below polls an API every 5 seconds and should increment a poll counter on each fetch. After the first poll the counter reads 1, but on every subsequent poll it stays at 1 instead of increasing. Identify the bug.#
Options
Show answer
The bug is that pollCount is read from a stale closure inside the useEffect. Because only query is in the dependency array, the pollCount captured in the effect body is frozen at its initial value of 0, so every setPollCount(pollCount + 1) evaluates to 0 + 1 = 1. The fix is to use a functional update — setPollCount((prev) => prev + 1) — which always operates on the latest committed state rather than the stale closure value.
The effect's dependency array is [query], so the effect body runs only when query changes. Inside the effect, pollCount is captured from the closure at the time the effect ran — its initial value of 0. Every subsequent call to setPollCount(pollCount + 1) evaluates 0 + 1 = 1, so the counter is reset to 1 on every poll and never advances. The fix is setPollCount((prev) => prev + 1), a functional update that React applies to the most recent committed state. Wrapping api.fetch in useCallback is unnecessary — the function is only used inside the effect and is not a dependency that changes behavior. setData(result) is a straightforward full replacement, not a merge issue. Adding pollCount to the dependency array would be harmful: the effect would tear down and recreate the interval on every count change, disrupting the 5-second cadence and defeating the purpose of the polling architecture.
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