React Mastery / Advanced React Hooks

useReducer Example

To truly understand useReducer, let's build a Shopping Cart. This is the perfect example because adding an item isn't just a simple update; it requires checking if the item exists, updating quantities, and calculating totals.


🖇️ The Shopping Cart Architecture

1. Define the Actions

In a professional setting, we use an object to store action names to prevent typos.

  • ADD_ITEM: Adds a product or increases quantity.

  • REMOVE_ITEM: Deletes an item from the cart.

  • CLEAR_CART: Resets everything.

File- 1 : ActionTypes.js

export const CART_ACTIONS = {
  ADD_ITEM: 'ADD_ITEM',
  REMOVE_ITEM: 'REMOVE_ITEM',
  CLEAR_CART: 'CLEAR_CART',
};

2. The Reducer Function

The reducer is the "brain." It looks at the current state and the action, then calculates the new state.

import { CART_ACTIONS } from './ActionTypes'; // Import constants

// 1. Define the Initial State
export const initialState = {
  items: [],
  total: 0,
};

// 2. Define the Reducer Function
export function cartReducer(state, action) {
  switch (action.type) {
    case CART_ACTIONS.ADD_ITEM: {
      const existingItemIndex = state.items.findIndex(item => item.id === action.payload.id);
      let updatedItems;

      if (existingItemIndex > -1) {
        // Create a new array and update the specific item's quantity
        updatedItems = [...state.items];
        updatedItems[existingItemIndex] = {
          ...updatedItems[existingItemIndex],
          quantity: updatedItems[existingItemIndex].quantity + 1
        };
      } else {
        // Add new item with quantity of 1
        updatedItems = [...state.items, { ...action.payload, quantity: 1 }];
      }

      return {
        ...state,
        items: updatedItems,
        total: state.total + action.payload.price,
      };
    }

    case CART_ACTIONS.REMOVE_ITEM: {
      const itemToRemove = state.items.find(item => item.id === action.payload.id);
      const filteredItems = state.items.filter(item => item.id !== action.payload.id);
      
      return {
        ...state,
        items: filteredItems,
        total: state.total - (itemToRemove.price * itemToRemove.quantity),
      };
    }

    case CART_ACTIONS.CLEAR_CART:
      return initialState;

    default:
      return state;
  }
}

3. The Component Implementation

Here is how we bring it all together in a React component using the dispatch function.

import React, { useReducer } from 'react';
import { cartReducer, initialState } from './CartReducer'; // Importing the logic
import { CART_ACTIONS } from './ActionTypes'; // Import constants

const PRODUCTS = [
  { id: 1, name: 'Mechanical Keyboard', price: 100 },
  { id: 2, name: 'Gaming Mouse', price: 50 },
];

export default function ShoppingCart() {
  // 3. Initialize useReducer
  const [state, dispatch] = useReducer(cartReducer, initialState);

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial' }}>
      <h2>Store Products</h2>
      <div style={{ display: 'flex', gap: '10px' }}>
        {PRODUCTS.map(product => (
          <button 
            key={product.id} 
            onClick={() => dispatch({ type: CART_ACTIONS.ADD_ITEM, payload: product })}
          >
            Add {product.name} (${product.price})
          </button>
        ))}
      </div>

      <hr />

      <h2>Your Cart</h2>
      <p><strong>Total Price: ${state.total}</strong></p>
      
      {state.items.length === 0 ? (
        <p>Your cart is empty.</p>
      ) : (
        <>
          <ul>
            {state.items.map(item => (
              <li key={item.id} style={{ marginBottom: '10px' }}>
                {item.name} — ${item.price} (x{item.quantity})
                <button 
                  style={{ marginLeft: '10px' }}
                  onClick={() => dispatch({ type: CART_ACTIONS.REMOVE_ITEM, payload: item })}
                >
                  Remove
                </button>
              </li>
            ))}
          </ul>
          <button onClick={() => dispatch({ type: CART_ACTIONS.CLEAR_CART })}>
            Clear Entire Cart
          </button>
        </>
      )}
    </div>
  );
}

Problem: If we remove the item from this, all the quantities will be removed. Solving it:

🧠 Deep Dive: How the logic flows

  1. The Trigger (Dispatch): When you click "Add Keyboard," you call dispatch({ type: 'ADD_ITEM', payload: product }).

  2. The Message (Action): The dispatch sends that object (the "Action") to the cartReducer.

  3. The Calculation (Reducer): The reducer sees the type is 'ADD_ITEM', takes the payload (the keyboard data), and uses the current state to calculate a new state object.

  4. The Re-render: React receives the new state object, notices the changes, and updates the UI automatically.


🚀 Why this is better than useState here:

If you used useState, your addItem function would be huge and living inside your UI component. With useReducer, all that "math" and "finding indexes" lives in a clean, separate function that you could even move to a different file and write tests for independently.