Recognizing when centralizing transition logic beats scattered setState calls.
useState is ideal for independent, simple pieces of state. Reach for useReducer when the next state depends on the previous state through non-trivial transitions, when several values change together as part of one logical event, or when update logic is complex enough to be worth naming (a reducer gives each transition an action name and a single place to read them all). A reducer is a pure (state, action) => newState function, so it is trivially unit-testable in isolation and keeps the component's event handlers thin — they just dispatch intent ({ type: 'increment' }) rather than computing new state inline. It also stabilizes callbacks: dispatch has a stable identity across renders, so passing it deep through context or memoized children avoids the referential-identity churn that setState-derived callbacks can cause. Rule of thumb: multiple related fields, or state whose transitions you'd otherwise duplicate across handlers -> useReducer; one or two loosely-related values -> useState. For truly global state, lift the reducer into context or use a store library.
Multi-field forms, wizards, undo/redo, or any component where transitions are complex or shared across handlers.