Skip to solution
mediumFrontend

What is the difference between `useState` and `useReducer`?

1.2k views
01

Understand the problem

Question presented to candidate: "You have a component whose state is getting messy — half a dozen useState calls that keep having to change together. When would you reach for useReducer instead, and what does that actually buy you?"

What a strong answer should cover:

  • Both add local state to a function component; they are equivalent in power.
  • useState fits simple, independent values updated directly.
  • useReducer centralises transitions in a pure reducer (state, action) => newState; components dispatch intent rather than computing the next state.
  • The real payoff: the reducer is a pure function testable with no React at all, and branchy transitions live in one place.
  • dispatch has a stable identity across renders, so it can be passed down without breaking React.memo — the same is true of the useState setter.
  • Multiple dispatches in one handler are batched into a single re-render.
  • Signal of depth: useState is implemented on the same underlying machinery as useReducer.

Clarifying questions expected:

  • "Do these state fields have to change together, or are they genuinely independent?"
  • "Is this state local, or does it need to be shared — in which case is a reducer plus context the right shape?"

Code / implementation expected: Yes — a small reducer with two or three action types, and the dispatch call site.

hooksstate managementuseStateuseReducer
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Frontend engineers preparing for React interviews — assumes familiarity with useState. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term: — the exact vocabulary an interviewer expects. Eve

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A reducer with a throwing default branch, plus batched dispatches
Run Playground
import { useReducer, useState, memo } from "react";

// Pure — no React, no side effects. This function alone is unit-testable:
//   expect(reducer({ count: 0 }, { type: "inc" })).toEqual({ count: 1 })
function reducer(state, action) {
  switch (action.type) {
    case "inc":   return { count: state.count + 1 };
    case "dec":   return { count: state.count - 1 };
    case "reset": return { count: 0 };
    // Throwing turns a typo in an action type into an immediate, obvious
    // error instead of a silent no-op.
    default: throw new Error("unknown action: " + action.type);
  }
}

// dispatch is referentially stable, so this memoised child never re-renders
// just because the parent did.
const Controls = memo(function Controls({ dispatch }) {
  console.log("Controls rendered");
  return (
    <p>
      <button onClick={() => dispatch({ type: "dec" })}>-</button>{" "}
      <button onClick={() => dispatch({ type: "inc" })}>+</button>{" "}
      <button onClick={() => dispatch({ type: "reset" })}>reset</button>{" "}
      {/* three dispatches, one re-render */}
      <button onClick={() => { dispatch({ type: "inc" }); dispatch({ type: "inc" }); dispatch({ type: "inc" }); }}>
        +3 (batched)
      </button>
    </p>
  );
});

export default function App() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  const [unrelated, setUnrelated] = useState(0);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <h3>Count: {state.count}</h3>
      <Controls dispatch={dispatch} />
      <button onClick={() => setUnrelated((n) => n + 1)}>
        Re-render parent ({unrelated})
      </button>
      <p style={{ color: "#666", fontSize: 13 }}>
        Watch the console: clicking the parent button re-renders App, but
        Controls does not re-render — dispatch keeps the same identity.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 28 of 119 decoded in the React.js track. One more won't hurt.

Back to track