- React
- Core React Hooks: State, Synchronization, and Refs
- useReducer: Predictable State Transitions
useReducer: Predictable State Transitions
useReducer manages complex state transitions by decoupling what happened (dispatching an action object) from how state updates (a pure reducer function). It is preferred over useState when state has interdependent fields or when passing dispatch down trees avoids callback re-creations.
Key Learning Objectives
Decouple action dispatching from state update logic using pure reducer functions.
Type action objects strictly using TypeScript discriminated unions.
Identify when useReducer is superior to multiple useState hooks.
Leverage stable dispatch function identity to optimize child component memoization.
The Interview Problem & Context
Implement a multi-field form state manager using useReducer with TypeScript discriminated union actions and explain why dispatch has a stable identity.
1import { useReducer } from 'react';23interface FormState {4 step: number;5 email: string;6 isSubmitting: boolean;7 error: string | null;8}910type FormAction =11 | { type: 'SET_EMAIL'; payload: string }12 | { type: 'NEXT_STEP' }13 | { type: 'SUBMIT_START' }14 | { type: 'SUBMIT_SUCCESS' }15 | { type: 'SUBMIT_ERROR'; error: string };1617function formReducer(state: FormState, action: FormAction): FormState {18 switch (action.type) {19 case 'SET_EMAIL':20 return { ...state, email: action.payload, error: null };21 case 'NEXT_STEP':22 return { ...state, step: state.step + 1 };23 case 'SUBMIT_START':24 return { ...state, isSubmitting: true, error: null };25 case 'SUBMIT_SUCCESS':26 return { ...state, isSubmitting: false, step: state.step + 1 };27 case 'SUBMIT_ERROR':28 return { ...state, isSubmitting: false, error: action.error };29 default:30 return state;31 }32}3334export function MultiStepForm() {35 const [state, dispatch] = useReducer(formReducer, {36 step: 1,37 email: '',38 isSubmitting: false,39 error: null,40 });4142 return (43 <div>44 <p>Step: {state.step}</p>45 <input46 value={state.email}47 onChange={e => dispatch({ type: 'SET_EMAIL', payload: e.target.value })}48 />49 <button onClick={() => dispatch({ type: 'NEXT_STEP' })}>Next</button>50 </div>51 );52}
Deep Architectural Explanation
When to Choose useReducer Over useState
1. Interdependent State Logic
If updating state variable A requires simultaneously changing state variable B (e.g., setting isLoading: true must also clear error: null), separate useState calls easily lead to inconsistent states. A reducer guarantees atomic, coordinated state transitions.
2. Stable dispatch Identity
React guarantees that the dispatch function reference is stable across all renders. It never changes:
// dispatch identity never changes:
const [state, dispatch] = useReducer(reducer, initialState);You can pass dispatch down via props or Context without needing useCallback, preventing re-renders in memoized child components.
3. Separation of Concerns
Reducers extract business logic out of the component JSX into a pure, testable function. You can write unit tests for formReducer(prevState, action) without rendering any React components or mocking the DOM!
Common Interview Traps & Pitfalls
Performing side effects (API calls, localStorage writes) inside the reducer function.
Mutating state directly in the reducer (e.g., state.step++ instead of { ...state, step: state.step + 1 }).
Using useReducer for simple, independent boolean flags where useState is clearer.
Senior Interviewer Follow-Up Probes
How does useReducer compare to Redux Toolkit slice reducers?
Can you implement useState using useReducer in one line of code?
How do you pass dispatch down through React Context to eliminate prop drilling of callbacks?
Frequently Asked Interview Questions
Is the dispatch function from useReducer stable across renders?
Yes. React guarantees that dispatch has a stable identity and will never change between renders, so it does not need to be listed in useEffect dependency arrays.
Why must the reducer function be pure?
Because React may execute reducers multiple times in Concurrent Mode or StrictMode. Impure reducers (mutating outside variables or triggering network calls) would cause duplicate side effects.
How do you type useReducer actions with TypeScript?
Using TypeScript Discriminated Unions where each action type is a distinct string literal with its own strictly typed payload.
How can useState be implemented using useReducer?
const useState = (initial) => useReducer((state, action) => typeof action === "function" ? action(state) : action, initial).
