React Curriculum

useReducer: Predictable State Transitions

Medium100% Free~25 mins#react#usereducer#reducer#state-machine#discriminated-unions#dispatch
Interview Answer (30-Second Summary)

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';
2
3interface FormState {
4 step: number;
5 email: string;
6 isSubmitting: boolean;
7 error: string | null;
8}
9
10type 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 };
16
17function 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}
33
34export function MultiStepForm() {
35 const [state, dispatch] = useReducer(formReducer, {
36 step: 1,
37 email: '',
38 isSubmitting: false,
39 error: null,
40 });
41
42 return (
43 <div>
44 <p>Step: {state.step}</p>
45 <input
46 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:

tsx
// 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
When should you use useReducer instead of useState?

When state has complex logic involving multiple sub-values, when the next state depends on the previous state in multiple branches, or when you want to pass dispatch down via context to avoid callback recreations.

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.

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.

Using TypeScript Discriminated Unions where each action type is a distinct string literal with its own strictly typed payload.

const useState = (initial) => useReducer((state, action) => typeof action === "function" ? action(state) : action, initial).