When component state is enough on mobile screens. Reach for useReducer when multiple fields update together - checkout steps, onboarding wizards, and validation-heavy forms. Keep API responses in TanStack Query; reducers own client UI transitions only.
dispatch from useReducer is stable for the component lifetime - omit it from useEffect dependency arrays. Same for setState from useState. The react-hooks lint rules document this.
Keep wizardReducer in the same file as OnboardingWizard until a second screen needs identical transitions. Promote to features/onboarding/wizardReducer.ts only on reuse - not preemptively.
Two fields (email, password) - useState is fine. If you add remember-me, MFA step, and inline validation, switch to useReducer or Hook Form.
Can I use useReducer with Context?
Yes - useReducer in a provider is the built-in global pattern. Split contexts or Zustand when consumers re-render too often - see Context Without Storms.
Does useReducer replace Redux?
Only for local or small shared UI state. Redux Toolkit still wins for audited multi-feature domains and DevTools at scale - see Redux Toolkit.
How do I test a reducer?
Export wizardReducer and unit-test actions without rendering:
expect(wizardReducer(initial, { type: "next" })).toEqual({ ...initial, error: "Name must be at least 2 characters" });
Should submitting be in the reducer?
Keep submitting as useState or derive from useMutationisPending. Async status is a side effect, not a pure transition.
What about React 19 useActionState?
useActionState targets Server Actions on web. Expo mobile apps use useReducer + TanStack Query useMutation for networked submits.
How many useState hooks is too many?
Rule of thumb: more than four related values on one screen → consider one reducer or a custom hook wrapping state.
Can I use immer with useReducer?
Yes via useImmerReducer from use-immer. Zustand + immer middleware is more common for global stores - see Zustand.
How do wizards interact with Expo Router?
Keep step index in reducer state for animated transitions. For shareable progress, mirror step in router.setParams({ step: "2" }) - URL is optional UX, reducer is source of truth during the session.
Why not put everything in one big useState object?
setState({ ...s, email }) works until two updates batch incorrectly or you forget a field. Reducers force explicit action names - easier to log and test.
How does this relate to container/presenter?
Container holds useReducer / useState; presenter receives props and callbacks only - see container-presenter on mobile.
Performance on long lists?
Reducer state in a parent of a 500-row FlatList re-renders all rows. Move list data to Query; keep selection in reducer with extraData or row-level memoization.