Topics in this subject
React 5 min read Updated 6 Aug 2026

useReducer

Reducer (state, action)=>newState centralizes complex state transitions; pair with Context for app-wide state.

🧑‍🏫 Sabse pehle — simple mein samjho#

Jab state simple ho (ek number, ek text) to useState kaafi hai. Lekin jab state pe kai tarah ke updates hone lagein (increment, decrement, reset...) to useReducer behtar hai. Reducer ek TV remote jaisa hai: tum ek action bhejte ho (button dabate ho, jaise "channel +1"), aur reducer function decide karta hai us action pe state kaise badlegi. Saari logic ek jagah rehti hai, component saaf rehta hai.

function reducer(state, action) {   // "is action pe aise badlo"
  if (action.type === "inc") return { count: state.count + 1 };
  if (action.type === "reset") return { count: 0 };
  return state;
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  // dispatch = "remote ka button dabao"
  return <button onClick={() => dispatch({ type: "inc" })}>{state.count}</button>;
}

Yaad rakho: useReducer = complex state ke liye remote — action bhejo, reducer decide karta hai kya hoga.

useReducer moves state-update logic out of the component into a pure reducer function. Prefer it over useState when updates are complex, interrelated, or follow explicit transitions.

Anatomy#

const initialState = { count: 0, step: 1 };

function reducer(state, action) {         // pure: (state, action) => newState
  switch (action.type) {
    case "increment": return { ...state, count: state.count + state.step };
    case "decrement": return { ...state, count: state.count - state.step };
    case "setStep":   return { ...state, step: action.payload };
    case "reset":     return initialState;
    default: throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "reset" })}>reset</button>
    </>
  );
}

🎯 The reducer must be pure: no mutations, no side effects, no async. Given the same (state, action) it returns the same next state. Side effects belong in event handlers or effects, not the reducer.

Dispatch flow#

flowchart LR
    UI["Component"] -->|"dispatch(action)"| R["reducer(state, action)"]
    R -->|"returns new state"| RE["React re-renders"]
    RE --> UI

Real-world example: a food-delivery cart reducer 🛒#

A Zomato/Swiggy-style cart is a great fit for useReducer: ADD_ITEM, REMOVE_ITEM, and CLEAR_CART are exactly the kind of related, mutually-exclusive transitions a reducer centralizes instead of scattering across button handlers.

const initialCart = { items: [], total: 0 };

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM": {
      const items = [...state.items, action.payload];
      return { items, total: items.reduce((sum, i) => sum + i.price, 0) };
    }
    case "REMOVE_ITEM": {
      const items = state.items.filter((i) => i.id !== action.payload.id);
      return { items, total: items.reduce((sum, i) => sum + i.price, 0) };
    }
    case "CLEAR_CART":
      return initialCart;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function MenuItem({ dish }) {
  const [cart, dispatch] = useReducer(cartReducer, initialCart);

  return (
    <>
      <button onClick={() => dispatch({ type: "ADD_ITEM", payload: dish })}>
        Add {dish.name} — ₹{dish.price}
      </button>
      <button onClick={() => dispatch({ type: "REMOVE_ITEM", payload: dish })}>
        Remove
      </button>
      <button onClick={() => dispatch({ type: "CLEAR_CART" })}>Clear cart</button>
      <p>Total: ₹{cart.total}</p>
    </>
  );
}

Every cart mutation goes through the same reducer, so "how does adding an item affect the total" logic lives in one testable place instead of being duplicated across handlers.

flowchart LR
    E["User taps 'Add to cart'"] --> D["dispatch action type ADD_ITEM payload dish"]
    D --> R["cartReducer(state, action)"]
    R --> N["new state: items + total"]
    N --> RR["React re-renders cart UI"]

Lazy initialization#

Pass a third init function to compute initial state lazily (e.g. reading localStorage once) instead of on every render:

const [state, dispatch] = useReducer(reducer, initialArg, (arg) => ({
  count: Number(localStorage.getItem("count")) || arg.count,
}));

useReducer vs useState 🎯#

Use useState when Use useReducer when
Independent primitive/simple values Multiple fields that change together
Few, simple updates Many action types / explicit transitions
Next state doesn't depend on complex prior state Next state derived from prior state + action
You want to test update logic in isolation (reducer is pure)
You pass updates deep (dispatch identity is stable)

🟢 dispatch has a stable identity across renders—React guarantees it never changes. You can pass it down through context or props without useCallback and without breaking memo.

useReducer + Context = app-wide state#

Combine a reducer for logic with context for distribution. Split state and dispatch into two contexts so dispatch-only components don't re-render on state changes.

const StateCtx = createContext(null);
const DispatchCtx = createContext(null);

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

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

This is essentially a mini-Redux with zero dependencies—good for medium apps that don't need selectors or middleware.

Immutable updates ⚠️#

Never mutate state; always return a new object/array. Mutation skips re-renders (React compares by reference) and breaks time-travel/undo.

// ⚠️ mutation — same reference, React may skip render
case "add": state.items.push(action.item); return state;

// 🟢 new references at every changed level
case "add": return { ...state, items: [...state.items, action.item] };

For deeply nested state, reach for Immer's produce to write "mutating" code that stays immutable.

Interview Q&A#

Q1. What signature does a reducer have and what constraint must it satisfy? (state, action) => newState. It must be pure: no mutation of state, no side effects, deterministic for the same inputs.

Q2. When do you prefer useReducer over useState? When state has multiple interrelated fields, many distinct transition types, or the next state depends on the previous state in non-trivial ways—centralizing logic in one testable reducer.

Q3. Why can you pass dispatch down without useCallback? React guarantees dispatch has a stable identity for the lifetime of the component, so it never triggers dependency-array changes or breaks memoized children.

Q4. How do you build app-wide state with just React? Combine useReducer (logic) with Context (distribution), typically splitting state and dispatch into separate contexts so dispatch-only consumers avoid re-rendering on state updates.

Q5. What breaks if a reducer mutates state instead of returning a new object? React compares state by reference; a mutation returns the same reference, so components may not re-render, and features like undo/redo and consistent snapshots break.