iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

useReducer

useReducer is useState for complex state transitions. Dispatch typed actions; a reducer pure-function returns the next state. Better than useState when state has many interrelated fields or transitions form a state machine.

Reducer, dispatch, with TS, with Context

EXAMPLE
import { useReducer } from 'react';

// 1) Define state + actions + reducer
type State = { count: number; busy: boolean; error: string | null };
type Action =
    | { type: 'increment' }
    | { type: 'decrement' }
    | { type: 'reset' }
    | { type: 'set';   value: number }
    | { type: 'start' }
    | { type: 'fail';  message: string };

function reducer(state: State, action: Action): State {
    switch (action.type) {
        case 'increment': return { ...state, count: state.count + 1 };
        case 'decrement': return { ...state, count: state.count - 1 };
        case 'reset':     return { count: 0, busy: false, error: null };
        case 'set':       return { ...state, count: action.value };
        case 'start':     return { ...state, busy: true,  error: null };
        case 'fail':      return { ...state, busy: false, error: action.message };
    }
}

const initial: State = { count: 0, busy: false, error: null };

function Counter() {
    const [state, dispatch] = useReducer(reducer, initial);
    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
            <button onClick={() => dispatch({ type: 'set', value: 10 })}>=10</button>
            <button onClick={() => dispatch({ type: 'reset' })}>reset</button>
        </div>
    );
}

// 2) Lazy init — runs initialiser once
function init(initialCount: number): State {
    return { count: initialCount, busy: false, error: null };
}

function Counter2({ start = 0 }) {
    const [state, dispatch] = useReducer(reducer, start, init);
    // ...
}

// 3) Async work via dispatch — the modern Redux-Toolkit pattern
async function load(dispatch: React.Dispatch<Action>) {
    dispatch({ type: 'start' });
    try {
        const value = await api.fetchCount();
        dispatch({ type: 'set', value });
    } catch (e) {
        dispatch({ type: 'fail', message: String(e) });
    }
}

useEffect(() => { load(dispatch); }, []);

// 4) Pair with Context — share state across deep trees without prop drilling
const StateCtx    = createContext<State>(null!);
const DispatchCtx = createContext<React.Dispatch<Action>>(null!);

export function StoreProvider({ children }: { children: React.ReactNode }) {
    const [state, dispatch] = useReducer(reducer, initial);
    return (
        <StateCtx.Provider value={state}>
            <DispatchCtx.Provider value={dispatch}>{children}</DispatchCtx.Provider>
        </StateCtx.Provider>
    );
}

export const useStore    = () => useContext(StateCtx);
export const useDispatch = () => useContext(DispatchCtx);

// 5) When to pick useReducer over useState
//   • State has multiple related fields that change together
//   • Updates depend on previous state + form a state machine
//   • You want a clear list of valid transitions (auditable)
//   • You're tempted to write a 200-line component with 8 useState calls

// 6) When useState is fine
//   • One value with simple transitions
//   • No complex update logic
//   • Small, isolated component

// 7) Tips
//   • Use discriminated-union action types — TS gives exhaustive checks
//   • Reducers must be PURE — no fetch, no setTimeout, no Math.random
//   • Avoid storing derived state — compute from state at render time (or via useMemo)
//   • For server state, prefer TanStack Query / SWR over a custom reducer

Why it matters

useReducer pays off when your component starts looking like a state machine. Discriminated-union actions + an exhaustive switch turn transitions into something the compiler verifies for you.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
const [state, dispatch] = useReducer(reducer, initial);
dispatch({ type: 'increment' });
Try it Yourself »

Discussion

Loading…