Debugging: every change passes through one place
The console is a full history: each action with the state before and after. The second decreased changes nothing (the minimum is 1), and you can see that at a glance. checkedOut changes three fields in one step.
Both hold the same kind of local state; choose useState for simple, independent values and useReducer when many handlers update the same state in related ways, when you want to read and debug every change as an action, or test the logic as a plain function, and add Immer when the updates are deeply nested.
Change the code and press Run (Ctrl+Enter). Predict what will happen first, then break it on purpose and read what changes. Your edits are saved and match the lesson page.
Ready for more? This phase has 5 hands-on exercises with automatic checks.
Practice questions
Write the code in the editor, run it, then open the model answer to compare.
Write a withLogger(reducer) wrapper that logs action.type and the next state, and use it with any reducer.
Write two tests (plain functions or Vitest) for a counterReducer with incremented and reset: one per action.
Explain it without notes
Compare useState and useReducer on code size, readability, debugging and testing.
When would you switch a component from useState to useReducer?
How do you test a reducer, and why is it easier than testing a component?
What is Immer, how does it work with reducers, and what are its two main rules?
Preview
Console
Nothing logged yet.Expected console output
increased: {"qty":1,"coupon":null,"status":"open"} -> {"qty":2,"coupon":null,"status":"open"}
decreased: {"qty":2,"coupon":null,"status":"open"} -> {"qty":1,"coupon":null,"status":"open"}
decreased: {"qty":1,"coupon":null,"status":"open"} -> {"qty":1,"coupon":null,"status":"open"}
couponApplied: {"qty":1,"coupon":null,"status":"open"} -> {"qty":1,"coupon":"CHAI10","status":"open"}
checkedOut: {"qty":1,"coupon":"CHAI10","status":"open"} -> {"qty":1,"coupon":null,"status":"paid"}