पाठ 15 / 25

useReducer for Complex State

Centralise update logic in a pure function.

Actions in, new state out

When state has several related parts or many ways to change, useReducer(reducer, initial) moves the update logic into a reducer: a pure function (state, action) => newState. Components dispatch actions describing what happened ({type: "add", item}), and the reducer decides how state changes. Because reducers are pure, they are easy to test without rendering anything, and all transitions are visible in one place.

A shopping cart reducer, run

I ran this with React 19.3 and react-dom 19.3 on Node.js 22, compiling JSX with esbuild 0.28. Browser-style examples use jsdom 29 as a fake DOM (imported from a small setup-dom.mjs file) and act() to apply updates. Adding a pen twice and a book gives "pen x2, book x1" with a total of 290; removing the pen leaves the book at 250. The reducer is also called directly, outside any component, to show it is a plain pure function.

import "./setup-dom.mjs";   // test setup: jsdom provides a browser-like DOM in Node
import { useReducer, act } from "react";
import { createRoot } from "react-dom/client";

function cartReducer(state, action) {
  switch (action.type) {
    case "add": {
      const found = state.find(i => i.id === action.item.id);
      return found ? state.map(i => i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i)
                   : [...state, { ...action.item, qty: 1 }];
    }
    case "remove": return state.filter(i => i.id !== action.id);
    default: throw new Error(`unknown action ${action.type}`);
  }
}
let api;
function Cart() {
  const [items, dispatch] = useReducer(cartReducer, []);
  api = dispatch;
  const total = items.reduce((s, i) => s + i.qty * i.price, 0);
  return <p>{items.map(i => `${i.name} x${i.qty}`).join(", ") || "empty"} | total {total}</p>;
}
const root = createRoot(document.getElementById("root"));
await act(() => root.render(<Cart />));
await act(() => api({ type: "add", item: { id: 1, name: "pen", price: 20 } }));
await act(() => api({ type: "add", item: { id: 1, name: "pen", price: 20 } }));
await act(() => api({ type: "add", item: { id: 2, name: "book", price: 250 } }));
console.log(document.querySelector("p").textContent);
await act(() => api({ type: "remove", id: 1 }));
console.log(document.querySelector("p").textContent);
console.log("reducer is a pure function:", JSON.stringify(cartReducer([], { type: "add", item: { id: 9, name: "x", price: 1 } })));

Output:

pen x2, book x1 | total 290
book x1 | total 250
reducer is a pure function: [{"id":9,"name":"x","price":1,"qty":1}]

Throw on unknown actions

A default case that throws catches typos in action types immediately.

त्वरित जाँच: What is a reducer?

  • A pure function that takes state and an action and returns new state
  • A function that mutates the DOM
  • A component that renders lists
  • A network request
Answer

A pure function that takes state and an action and returns new state — Pure and testable.