ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

useState vs useReducer in React

Short answer

Both store state in a function component and both re-render on update. useState holds one value you replace directly. useReducer holds a value you update by dispatching an action to a pure reducer function. Switch to useReducer when several pieces of state change together, or when the next state depends on the current one in non-trivial ways.

useStateuseReducer
You callsetValue(next)dispatch({ type, payload })
Update logic livesAt each call siteIn one reducer function
Best forIndependent, simple valuesSeveral fields that change together
Next state from previoussetValue(v => …)Built in — the reducer receives it
Setter identityStableStable
Testable without ReactNoYes — the reducer is a pure function
BoilerplateAlmost noneA reducer plus action types
DebuggabilityScattered settersOne place to log every transition
Same capability, different shape — the difference is where update logic lives.

They do the same job

Start from the fact that these are not different capabilities. useState is implemented in terms of the same machinery as useReducer — internally, useState is a useReducer with a built-in reducer that simply returns whatever you passed. Anything you can build with one you can build with the other. The choice is entirely about how the update logic is organised and how legible it stays as the component grows.

The same counter, written both waysjsx
1// useState
2const [count, setCount] = useState(0);
3<button onClick={() => setCount(c => c + 1)}>+</button>
4// → the "what happens next" logic lives in the JSX
5
6// useReducer
7function reducer(state, action) {
8  switch (action.type) {
9    case "inc":   return { count: state.count + 1 };
10    case "reset": return { count: 0 };
11    default:      return state;
12  }
13}
14const [state, dispatch] = useReducer(reducer, { count: 0 });
15<button onClick={() => dispatch({ type: "inc" })}>+</button>
16// → the JSX only names the intent; the logic lives in one function

For a counter, useReducer is plainly worse — more code for identical behaviour. That is worth stating outright, because a lot of tutorial code reaches for reducers far too early. The reducer earns its keep when there is enough logic that having it in one place is a relief rather than a ceremony.

The signal: state that changes together

The reliable trigger is not the number of useState calls — it is whether they are coupled. Four independent booleans are perfectly fine as four useState calls. But when submitting a form must set loading to true, clear the error, and blank the result, all three are one transition, and useState forces you to spell that transition out at every place it can happen.

The coupling problem, and the reducer that removes itjsx
1// Three useStates, one logical transition, repeated at every call site
2const [loading, setLoading] = useState(false);
3const [error, setError]     = useState(null);
4const [data, setData]       = useState(null);
5
6async function submit() {
7  setLoading(true); setError(null); setData(null);   // must not forget one
8  try   { setData(await save()); }
9  catch (e) { setError(e); }
10  finally   { setLoading(false); }
11}
12// → forget setError(null) on a retry and the old error stays
13//   on screen next to fresh data
14
15// One reducer: each transition is named and complete
16function reducer(state, action) {
17  switch (action.type) {
18    case "submit":  return { loading: true,  error: null,       data: null };
19    case "success": return { loading: false, error: null,       data: action.data };
20    case "failure": return { loading: false, error: action.error, data: null };
21    default: return state;
22  }
23}
24// → "submit" cannot half-happen; the whole shape is written once

That is the crux. The reducer version does not have fewer lines; it has fewer places where the invariant can be broken. Every reviewer can see the complete set of transitions by reading one function, and adding a fourth field means editing one place rather than auditing every handler.

Making impossible states impossible

The three-boolean version has eight combinations, of which several are nonsense — loading and error at the same time, data present while still loading. A reducer lets you collapse those into a single status field, so the impossible combinations cannot be represented at all. This is where reducers stop being organisational and start being a correctness tool.

One status instead of three flagsjsx
1const initial = { status: "idle", data: null, error: null };
2
3function reducer(state, action) {
4  switch (action.type) {
5    case "submit":  return { status: "loading", data: null, error: null };
6    case "success": return { status: "done",    data: action.data, error: null };
7    case "failure": return { status: "error",   data: null, error: action.error };
8    case "retry":   return state.status === "error" ? reducer(state, { type: "submit" }) : state;
9    default:        return state;
10  }
11}
12// → status is exactly one of four values; "loading AND error"
13//   is now unrepresentable rather than merely unlikely
14
15// Rendering follows the status directly, with no flag juggling:
16if (state.status === "loading") return <Spinner />;
17if (state.status === "error")   return <Error err={state.error} />;

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399

dispatch is stable, which has real consequences

React guarantees that the dispatch function returned by useReducer is the same reference for the life of the component. So is the setter from useState, but the difference shows up when you pass behaviour down a tree. With useState you often pass several setters plus wrapper functions that encode logic, and those wrappers need useCallback to stay stable. With useReducer you pass one already-stable dispatch, and the logic stays in the reducer where it does not need memoising at all.

Fewer things to memoisejsx
1// useState: the wrapper is new every render, so memoised children
2// re-render unless you wrap it
3const addItem = useCallback(
4  (item) => setItems(prev => [...prev, item]),
5  []
6);
7<MemoList onAdd={addItem} />
8
9// useReducer: dispatch is already stablenothing to memoise
10<MemoList dispatch={dispatch} />
11// → MemoList bails out correctly with no useCallback anywhere

The same property makes reducers a good fit for effects. Referencing dispatch inside a useEffect never causes the effect to re-run, whereas a handler built from setState plus surrounding values often does — which is a frequent cause of effects firing far more often than intended.

Reducers are testable without React

A reducer is a pure function of state and action. You can test every transition of a complex component without rendering anything, without a testing library, and without simulating clicks. That is a meaningful difference for state machines like checkout flows, multi-step forms and editors, where the interesting bugs are in the transitions rather than the markup.

A whole flow, tested in millisecondsjavascript
1const afterSubmit = reducer(initial, { type: "submit" });
2console.log(afterSubmit.status);
3// → "loading"
4
5const afterFail = reducer(afterSubmit, { type: "failure", error: new Error("nope") });
6console.log(afterFail.status, afterFail.data);
7// → "error" null
8
9// The invariant, asserted directly:
10console.log(reducer(afterFail, { type: "success", data: 1 }).error);
11// → null   (a success must always clear the previous error)

useReducer with context: a store without a library

Put the reducer's state and dispatch into context and you have a small global store: a single place transitions are defined, and any component in the tree can dispatch. This covers a lot of ground that people install Redux for, with no dependency. Its limits are real, though — every consumer re-renders when the state object changes, so it suits state that changes occasionally, not on every keystroke.

Two contexts, so dispatchers do not re-render on every changejsx
1const StateCtx = createContext(null);
2const DispatchCtx = createContext(null);
3
4function Provider({ children }) {
5  const [state, dispatch] = useReducer(reducer, initial);
6  return (
7    <StateCtx.Provider value={state}>
8      <DispatchCtx.Provider value={dispatch}>{children}</DispatchCtx.Provider>
9    </StateCtx.Provider>
10  );
11}
12// → components that only dispatch subscribe to DispatchCtx, whose
13//   value never changes, so they never re-render on a state update

Splitting the contexts is the trick worth knowing. A single context carrying both means a component that only ever dispatches still re-renders on every state change, because context propagates by reference and the tuple is new each time.

A migration recipe

You rarely design a reducer up front. The realistic path is that a component accumulates useState calls until the coupling becomes uncomfortable, and then you convert. Doing that in a fixed order keeps it mechanical rather than a rewrite.

  1. Group the coupled useState values into one object and write it as the initial state.
  2. List every event handler that changes more than one of them — each becomes one action.
  3. Name the actions after what happened, not what to set: "submitted", "itemRemoved", not "setLoading".
  4. Move each handler's body into the reducer, returning a complete new state for that transition.
  5. Replace each handler with a single dispatch of the matching action.
  6. Look for flags that are always mutually exclusive and collapse them into one status field.
  7. Leave genuinely independent values as useState — mixing the two in one component is normal and fine.
SymptomSuggestsReason
One or two unrelated valuesuseStateA reducer adds ceremony for nothing
Three or more values changing in stepuseReducerOne transition, one place
Handlers that set several states in sequenceuseReducerThe sequence is the transition
Flag combinations that should never occuruseReducerCollapse into a status union
Deeply passed update logicuseReducerdispatch is stable; wrappers are not
Transitions worth unit-testinguseReducerThe reducer is pure
A single form inputuseStateNothing to coordinate
A quick read on which one a component wants.

Is useReducer faster than useState?

Not meaningfully — both schedule a re-render the same way. The performance argument is indirect: dispatch is stable, so passing it down avoids the useCallback wrappers that setState-based handlers need, which lets React.memo actually bail out. That is a structural win, not a faster update.

Can I use both in the same component?

Yes, and it is often the right shape. Put the coupled fields in a reducer and leave independent values like a UI toggle in useState. Forcing every value into one reducer produces a giant state object where unrelated things sit next to each other for no reason.

Do I still need Redux?

For most apps, no. useReducer plus context covers shared client state, and server data belongs in React Query or SWR rather than in either. Redux Toolkit still earns its place for large apps that want devtools time-travel, middleware and a mature ecosystem around a single global store.

What is the third argument to useReducer?

An init function: useReducer(reducer, arg, init) calls init(arg) to compute the initial state lazily, so expensive setup does not run on every render. It also gives you a clean way to implement reset — dispatch a reset action whose handler returns init(arg) again.

Should actions be strings or objects?

Objects with a type field are the convention, because they carry a payload and read well in logs and devtools. A plain string works for reducers with no payload. What matters more is naming actions after events that happened rather than after the setters they call.

Frequently asked questions

What is the difference between useState and useReducer?
useState gives you a value and a setter you call with the next value. useReducer gives you a value and a dispatch function you call with an action, which a pure reducer turns into the next state. Both re-render on change; useReducer centralises the update logic in one function.
When should I use useReducer instead of useState?
When several pieces of state change as one transition, when the next state depends on the current one in more than a trivial way, when flag combinations exist that should be impossible, or when you want to unit-test the transitions without rendering the component.
Is useReducer faster than useState?
No. Both trigger a re-render identically. The indirect performance benefit is that dispatch is referentially stable, so passing it to memoised children avoids the useCallback wrappers that setState-based handlers require.
Can I use useState and useReducer together?
Yes, and it is usually the cleanest result. Group the coupled fields into a reducer and leave independent values — a dropdown's open flag, a hover state — in useState. There is no benefit to cramming unrelated state into one reducer.
What is a reducer function in React?
A pure function with the signature (state, action) => newState. It must not mutate the state it receives, must not perform side effects, and must return the existing state for actions it does not recognise. Being pure is what makes it testable and predictable.
Does useReducer replace Redux?
For local and moderately shared state, largely yes — especially combined with context. Redux Toolkit still adds value for large applications wanting devtools time-travel, middleware, and one well-understood global store shape. Server data belongs in React Query rather than either.
Why is my component not re-rendering after dispatch?
Usually because the reducer mutated and returned the same object. React compares by reference, so state.items.push(x); return state; looks unchanged. Return a new object with the spread syntax, and produce new arrays with map, filter or a spread.
Is the dispatch function stable across renders?
Yes. React guarantees dispatch keeps the same identity for the lifetime of the component, so it is safe to pass to memoised children and to omit from effect dependency arrays without lying to the linter.
How do I set initial state lazily with useReducer?
Pass a third argument: useReducer(reducer, arg, init) calls init(arg) once to compute the initial state, so expensive setup does not rerun on every render. It also makes reset easy — return init(arg) from a reset action.
How do I handle async operations with useReducer?
The reducer stays pure; the async work lives outside it. Dispatch a start action, await the promise in an event handler or effect, then dispatch success or failure with the result. Anything else — a fetch inside the reducer — breaks purity and misbehaves under Strict Mode.
What is the useReducer and context pattern?
Put the reducer's state in one context and its dispatch in a second, then wrap the tree in both providers. Any component can dispatch, and components that only dispatch never re-render on a state change because the dispatch context value never changes.
Should I use useReducer for form state?
For a form with a handful of independent inputs, useState per field is simpler. A reducer starts to pay off with multi-step forms, cross-field validation, or dirty and submitting flags that must move in step — at which point a form library is also worth considering.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399
Written by Arun Karthikeyan · Last updated

Full kit

React Interview Kit · ₹399

Get it →