useState Interview Questions & Answers
34 questions Updated 2026-06-17
React useState interview questions — state updates, batching, functional updates, lazy initialization and why state seems one render behind.
Read the in-depth guideReact useState Hook — A Complete Guide with ExamplesuseState returns an array of exactly two elements: the current state
value for this render, and a setter function that schedules an update
and triggers a re-render. You destructure them, and the [value, setValue]
naming convention is just that — a convention, not something React enforces.
const [count, setCount] = useState(0)
// ▲ current value ▲ setter that re-renders with the next value
A few things that trip people up in interviews:
- The argument (
0here) is only the initial value. On every render after the first, React ignores it and hands you the latest stored value. - The setter has a stable identity — React guarantees it never changes
between renders, so it's safe to omit from
useEffect/useCallbackdependency arrays. - Calling the setter does not mutate
countin the current scope; it asks React to render again with a new value.
Because count is a const captured by this render's closure, not a live
reference to a mutable box. Each render gets its own count constant frozen
at the value it had when that render ran. Calling setCount schedules a
future render with a new value — it cannot reach back and change the
count you're currently looking at.
function handleClick() {
console.log(count) // e.g. 0
setCount(count + 1) // schedules a render where count will be 1
console.log(count) // STILL 0 — same render, same frozen constant
}
So state isn't really "behind" — you're reading a snapshot. The new value
becomes visible only in the next render's function body. If you need the
updated value immediately after setting it, derive it locally
(const next = count + 1) or read it in an effect that runs after the
re-render.
Use the functional form — setCount(c => c + 1) — whenever the next state is
derived from the previous state, especially when several updates happen in
one event or across async boundaries. React passes the most up-to-date value
into your updater, so you never base a calculation on a stale snapshot.
// All three read the same stale `count`, so this adds 1, not 3
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
// Each updater receives the result of the previous one -> +3
setCount(c => c + 1)
setCount(c => c + 1)
setCount(c => c + 1)
Rule of thumb: if the new value mentions the old value, prefer the function
form. If you're setting an unrelated value (setCount(0)), the direct form
is fine.
If your initial value is expensive to compute, pass a function to
useState instead of the value itself. React calls that function only on
the first render and ignores it afterwards. Passing the value directly would
re-run the expensive computation on every render and then throw the result
away.
// readFromLocalStorage() runs on every single render
const [items, setItems] = useState(readFromLocalStorage())
// runs once, on mount only
const [items, setItems] = useState(() => readFromLocalStorage())
Watch the distinction: useState(expensive()) calls expensive every
render (its return value is the argument), whereas useState(expensive) /
useState(() => expensive()) hands React the function to call once.
Unlike this.setState in class components, the useState setter replaces
the value — it does not shallow-merge. To change one field of an object you
must spread the previous object yourself and override the field, returning a
new object (mutating the existing one won't trigger a re-render because the
reference is unchanged).
const [user, setUser] = useState({ name: 'Ada', age: 36 })
// new object, old fields preserved, name overridden
setUser(u => ({ ...u, name: 'Grace' }))
// mutates in place — same reference, React skips the re-render
user.name = 'Grace'
setUser(user)
For deeply nested state this spreading gets verbose; that's often the signal
to reach for useReducer or a state library.
Batching is React grouping multiple state updates into a single re-render
for performance, instead of re-rendering once per setState call. If you call
three setters in one click handler, React processes them together and renders
once.
function handleClick() {
setA(1)
setB(2)
setC(3)
// ONE re-render, not three
}
Before React 18, batching only happened inside React event handlers; updates
in setTimeout, promises, or native event listeners each caused their own
render. React 18's automatic batching extends grouping to those async
contexts too. If you ever need to opt out and force a synchronous, separate
render, wrap the update in flushSync from react-dom.
Usually no. If a value can be calculated from existing props or state,
derive it during render instead of duplicating it in useState — extra
state can drift out of sync and forces you to keep two things updated.
// redundant state that can desync
const [items, setItems] = useState([])
const [count, setCount] = useState(0)
// derive it
const [items, setItems] = useState([])
const count = items.length
Only store something in state if it's genuinely independent input. "You might not need state" is the React team's own guidance.
When two components need to share or stay in sync over the same data, you move the state to their closest common parent and pass it down as props plus a setter callback. The parent becomes the single source of truth.
function Parent() {
const [value, setValue] = useState('')
return (
<>
<Input value={value} onChange={setValue} />
<Preview value={value} />
</>
)
}
It's the standard fix for "these siblings need the same data." When lifting gets painful across many levels, that's the signal to reach for Context or a store.
Use useState for values that should trigger a re-render when they change.
Use useRef for mutable values that should persist across renders but NOT
cause re-renders — timer ids, previous values, DOM nodes.
const [count, setCount] = useState(0) // UI depends on it -> re-render
const renders = useRef(0) // bookkeeping -> no re-render
renders.current++
Changing ref.current is invisible to React's render cycle. If the screen needs
to reflect the value, it belongs in state; otherwise a ref avoids needless
renders.
The cleanest way is to change the component's key. React treats a new key
as a brand-new component, unmounting the old instance (discarding its state) and
mounting a fresh one — no manual reset code.
<Profile key={userId} userId={userId} />
// when userId changes, Profile remounts with fresh state
Alternatives are manually calling setters back to initial values, but key is
idiomatic for "start this subtree over." Avoid resetting state inside an effect
by watching a prop — the key approach is simpler and bug-free.
Because the useState setter treats a function argument as an updater, you
must wrap a function you want to store in another function — otherwise React
calls it instead of saving it.
// React invokes handleClick to compute the next state
const [fn, setFn] = useState(handleClick)
setFn(handleClick)
// wrap it so it's stored, not called
const [fn, setFn] = useState(() => handleClick)
setFn(() => handleClick)
The same rule applies to lazy initialization. Storing functions in state is rare — usually a ref or just defining the function in render is cleaner.
Treat the array as immutable — never push/splice the existing array
(same reference -> no re-render). Build a new array with spread or filter.
// add
setItems(prev => [...prev, newItem])
// remove by id
setItems(prev => prev.filter(it => it.id !== id))
// insert at index
setItems(prev => [...prev.slice(0, i), newItem, ...prev.slice(i)])
Using the functional updater (prev =>) keeps you correct when several updates
batch together.
Map over the array, returning a new object for the matching item and the originals for the rest. Mutating the found object in place won't re-render and can corrupt previous renders.
setUsers(prev =>
prev.map(u => u.id === id ? { ...u, name: 'Ada' } : u)
)
The rule: new array and new object for whatever you change. For deeply
nested updates this gets verbose — a signal to use useReducer or Immer.
Reach for useReducer when state is complex (multiple sub-values that change
together), when the next state depends on intricate logic, or when you want
to centralize update logic in one tested function instead of scattering
setters.
const [state, dispatch] = useReducer(reducer, initial)
dispatch({ type: 'increment', by: 2 })
useState is best for simple, independent values. A heuristic: if you find
yourself calling several setters together or your setter logic is branchy, a
reducer makes intent clearer and easier to test.
A controlled input gets its value from state and updates state on every
keystroke via onChange, making React the single source of truth.
const [text, setText] = useState('')
<input value={text} onChange={e => setText(e.target.value)} />
Forgetting onChange while setting value makes the field read-only (React
warns). The benefit is you can validate, format, or react to every change; the
cost is a re-render per keystroke (rarely a problem).
- Controlled — React state holds the value (
value+onChange). Predictable and easy to validate, but re-renders on each change. - Uncontrolled — the DOM holds the value; you read it via a
refwhen needed (defaultValuefor the initial value).
// uncontrolled
const ref = useRef()
<input defaultValue="hi" ref={ref} />
// read ref.current.value on submit
Controlled is the default recommendation; uncontrolled suits simple forms, file inputs, or integrating non-React widgets.
Prefer multiple useState calls for values that change independently — it's
simpler and you don't have to spread-merge on every update. Group into one object
only when fields genuinely change together.
// independent values
const [name, setName] = useState('')
const [age, setAge] = useState(0)
// grouping requires manual merge (no auto-merge like class setState)
setForm(prev => ({ ...prev, name: 'Ada' }))
Remember the useState setter replaces, it doesn't merge — so an object of
state means spreading the rest every time.
Spread at every level you change, all the way down — only the touched branches get new references.
setUser(prev => ({
...prev,
address: { ...prev.address, city: 'Paris' },
}))
This is error-prone and verbose for deep trees. Options: restructure to flatter
state, switch to useReducer, or use Immer (produce) which lets you
"mutate" a draft and produces the immutable update for you.
Redundant state is data you keep in useState that's already derivable from
other state or props. It invites bugs because you must remember to update it
everywhere, and the copies can disagree.
// fullName must be kept in sync manually
const [fullName, setFullName] = useState('')
// derive it
const fullName = `${first} ${last}`
Keep state minimal and orthogonal — the smallest set of independent values from which everything else is computed during render.
Use the functional updater so you always flip the latest value, which matters if toggles can batch.
const [open, setOpen] = useState(false)
const toggle = () => setOpen(o => !o)
Avoid setOpen(!open) in code paths that may run multiple times in one event —
it reads a possibly stale open. The functional form is always safe.
A callback captures the state value from the render it was created in, so after
an await it may be stale. To act on the latest value, use the functional
updater (which receives the current value) or store it in a ref.
async function save() {
await delay(1000)
// `count` is whatever it was when save() was created
// read the latest via the updater
setCount(latest => { send(latest); return latest })
}
Cleaner still: pass the needed value as an argument, or keep a useRef mirror of
the state for "read latest" access without triggering renders.
If you set state to a value that's Object.is-equal to the current one,
React bails out and skips re-rendering that component (it may still re-run
the component once to check, then stop).
const [n, setN] = useState(0)
setN(0) // same value -> React bails out, no committed re-render
The catch: this is reference equality. setItems([]) with a new empty
array is a different reference, so it does re-render even though the contents
look identical. Don't create fresh objects/arrays for "no change."
- State is data a component owns and can change over time via its setter.
- Props are data passed in from a parent; the child treats them as read-only.
function Counter({ step }) { // step is a prop (read-only)
const [count, setCount] = useState(0) // count is state (owned)
return <button onClick={() => setCount(c => c + step)}>{count}</button>
}
A child never mutates props; to change parent data it calls a callback prop. One component's state is often another's props (passed down).
React identifies each hook by its call order, not a name. Calling useState
inside a condition, loop, or after an early return changes that order between
renders, so React mismatches state to the wrong hook.
// breaks hook ordering
if (loggedIn) {
const [name, setName] = useState('')
}
// always call unconditionally; branch on the value
const [name, setName] = useState('')
if (loggedIn) { /* use name */ }
Always call hooks at the top level of the component, in the same order every
render — the eslint-plugin-react-hooks rule enforces this.
Two common cases: (1) you mutated the existing object/array and passed the
same reference, so React sees no change; or (2) you set a primitive to the
same value (React bails out via Object.is).
// mutate + same reference -> no re-render
user.name = 'Ada'
setUser(user)
// new reference
setUser({ ...user, name: 'Ada' })
The fix is always to produce a new reference for changed objects/arrays. If the UI "isn't updating," this mutation trap is the first thing to check.
Generally you should not — setting state in the render body unconditionally causes an infinite loop. React does support a narrow pattern: calling a setter conditionally during render to adjust state based on a prop change, which it handles without an extra paint.
// rare, allowed: derive on prop change without an effect
const [prevId, setPrevId] = useState(id)
if (id !== prevId) {
setPrevId(id)
setSelection(null) // reset when id changes, no effect needed
}
Most of the time you don't need this — prefer deriving values or the key reset
trick. Never call a setter unconditionally in render.
Calling a setter unconditionally during render, or inside an effect whose dependencies you keep changing, makes React render -> set -> render forever.
// sets state every render -> infinite loop
const [n, setN] = useState(0)
setN(n + 1)
// effect with a new object dep each render
useEffect(() => setData(load()), [{}])
Fixes: only set state in event handlers or effects with stable deps, derive values instead of storing them, and avoid fresh object/array literals in dependency arrays.
The useState initializer is only read on the first render. Passing a prop
as the initial value captures it once; later changes to the prop don't flow into
the state.
function Field({ initial }) {
const [value, setValue] = useState(initial) // only the first `initial` is used
// later `initial` changes are ignored
}
If you truly need to reset when the prop changes, use the key prop to
remount, or the conditional set-during-render pattern. Often the better answer is
to not copy the prop into state at all.
Use useMemo — it recomputes the value only when its dependencies change, so you
get the benefit of a cached derivation without the desync risk of putting it
in state.
const sorted = useMemo(
() => [...items].sort(compare),
[items]
)
This keeps items as the single source of truth while avoiding re-sorting on
every keystroke. Don't reach for useMemo until the computation is actually
expensive — needless memoization adds its own overhead.
Keep state as close as possible to where it's used (colocation), and lift it only as high as the nearest common ancestor that needs to share it. Over-lifting state to the top causes unnecessary re-renders and prop drilling.
// a modal's "open" state belongs in the component that owns the modal,
// not in the app root
function Toolbar() {
const [open, setOpen] = useState(false)
// ...
}
Colocated state means fewer renders and simpler components; global/app state should be reserved for genuinely shared data.
Hold the fields in one object and use a generic change handler keyed by the
input's name, spreading the previous object to preserve the other fields.
const [form, setForm] = useState({ name: '', email: '' })
const onChange = e =>
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
<input name="email" value={form.email} onChange={onChange} />
The computed key [e.target.name] updates just one field. For large/validated
forms, a form library or useReducer scales better than hand-rolling this.
No. The class this.setState(value, callback) second argument doesn't exist for
the useState setter. To run code after a state update commits, use a
useEffect that depends on that state.
const [count, setCount] = useState(0)
useEffect(() => {
// runs after the render caused by count changing
analytics.track(count)
}, [count])
This is the hooks way to "do X after state changes" — react to the new value in an effect rather than passing a callback to the setter.
An <input>'s value is always a string, so convert it when you need a
number, and decide how to handle empty/invalid input.
const [age, setAge] = useState('')
<input
type="number"
value={age}
onChange={e => setAge(e.target.value)} // keep the raw string in state
/>
const ageNum = age === '' ? 0 : Number(age) // parse where you use it
Storing the raw string avoids fighting the input (e.g. a partially typed - or
.), and you parse to a number only at the point of use.
const [count, setCount] = useState(0)
function handle() {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
} // after one click, count is 1 — not 3
All three read the same count (0) from this render's closure, each
computing 1, and batching collapses them to a single update of 1. Use the
functional form to actually add 3:
setCount(c => c + 1) // ×3 -> each receives the previous result -> 3
This puzzle tests whether you understand closures-over-state plus batching.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.