Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 81

Updating Cart Item Quantities

Recommended resources

Hello! Welcome back to our e-commerce project.

In the last lesson, we built a fantastic, read-only shopping cart page. It successfully subscribes to our CartContext, displays all the items, and calculates the total price. Now, it's time to make it interactive.

Today's goal is to implement functionality to update item quantities in the cart. We will add + and - controls, define the logic to handle these changes in our cartReducer, and dispatch the corresponding actions from our UI. This will bring our cart one step closer to being fully functional and is a core pattern in managing complex state.

1. The Reducer Pattern for State Updates

Before we write any code, let's briefly revisit why we're using useReducer. For an action like updating a quantity, we could be tempted to manage it with useState inside the CartPage component. However, this logic is central to the cart's state. By keeping it inside the reducer, we centralize all state mutations (add, update, remove) in one predictable place.

The pattern is always the same:

  1. The UI component dispatches an action object (e.g., { type: 'INCREASE_QUANTITY', payload: { id: 123 } }).
  2. The useReducer hook sends this action to our reducer function.
  3. The reducer contains the logic to create a new state based on that action.
  4. React re-renders the components that use the updated state.

This separation of concerns—where the component says what to do and the reducer says how to do it—is what makes useReducer so powerful for managing state in larger applications.

What is the use of useReducer ? - GeeksforGeeks

The GeeksforGeeks article, "What is the use of useReducer?", provides a great overview of this pattern. It specifically highlights managing a shopping cart as a primary use case.

Please read the introduction and the "Approach" section. Also, review the "UseCases for useReducer" list. Focus on how it describes the reducer's role in handling actions like adding, removing, and updating quantities.


2. Updating the Cart Reducer

Our first step is to teach our cartReducer how to handle quantity changes. This involves defining new action types and adding the corresponding logic.

Open your src/context/CartContext.jsx file where your cartReducer is located.

Step 1: Add New Cases to the Reducer

We'll add two new cases to our switch statement: INCREASE_QUANTITY and DECREASE_QUANTITY.

The logic for both will be similar:

  1. Find the item in the state.items array that matches the action.payload.id.
  2. Create a new items array using .map(). This is crucial for immutability—we never modify the original state directly.
  3. If the item is the one we want to update, we return a new object for it with the modified quantity.
  4. Otherwise, we return the item unchanged.

Here's the updated cartReducer:

// src/context/CartContext.jsx

// ... (your existing imports and initial state)

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      // ... (your existing ADD_ITEM logic)
      // No changes here

    // --- NEW LOGIC STARTS HERE ---
    case 'INCREASE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 } // Create a new item object with updated quantity
            : item // Return the original item if ID doesn't match
        ),
      };

    case 'DECREASE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: Math.max(1, item.quantity - 1) } // Use Math.max to prevent quantity < 1
            : item
        ),
      };
    // --- NEW LOGIC ENDS HERE ---

    default:
      return state;
  }
};

// ... (your CartProvider and useCart hook)

By using .map(), we adhere to a core principle of reducers: returning a brand new state object. The Math.max(1, ...) logic provides a simple business rule: the quantity cannot drop below 1.


3. Implementing the UI Controls

Now that our reducer is ready, let's add the + and - buttons to CartPage.jsx and wire them up to dispatch our new actions.

Open src/pages/CartPage.jsx.

First, get the dispatch function from our useCart hook.

// src/pages/CartPage.jsx
// ... imports
import { useCart } from '../context/CartContext';

const CartPage = () => {
  const { state, dispatch } = useCart(); // Get dispatch
  // ... rest of the component

Next, find the section inside your .map() loop where the quantity is displayed. We'll replace the static text with a more interactive set of controls.

Here is the updated JSX for the item display within your items.map() call.

// Inside the return statement of CartPage.jsx, within the items.map(...) callback

<div key={item.id} className="flex items-center border p-4 rounded-lg shadow-sm bg-white">
  <img src={item.image} alt={item.title} className="w-20 h-20 object-contain mr-4" />
  <div className="flex-grow">
    <h2 className="text-lg font-semibold">{item.title}</h2>
    <p className="text-gray-600">${item.price.toFixed(2)}</p>
  </div>
  
  {/* --- NEW CONTROLS START HERE --- */}
  <div className="flex items-center gap-4">
    <button
      onClick={() => dispatch({ type: 'DECREASE_QUANTITY', payload: { id: item.id } })}
      className="px-3 py-1 border rounded-md hover:bg-gray-100"
    >
      -
    </button>
    <span className="font-semibold">{item.quantity}</span>
    <button
      onClick={() => dispatch({ type: 'INCREASE_QUANTITY', payload: { id: item.id } })}
      className="px-3 py-1 border rounded-md hover:bg-gray-100"
    >
      +
    </button>
  </div>
  {/* --- NEW CONTROLS END HERE --- */}

  <div className="text-lg text-right w-24">
    <p className="font-bold mt-2">${(item.price * item.quantity).toFixed(2)}</p>
  </div>
</div>

With these changes, your buttons are now connected. When you click them, they dispatch the appropriate action with the item's id. The reducer handles the logic, a new state is created, and your component re-renders to show the updated quantity, item subtotal, and order total automatically!


4. Visualizing the Complete Flow

To see this entire process in action, the following video provides a helpful walkthrough. It builds the same functionality: creating handler functions, dispatching actions, and updating the reducer.

Build a Shopping Cart with React JS, Context API, and useReducer Hook | Ecommerce Website Tutorial

This segment from the 'Build a Shopping Cart' tutorial by Code With Yousaf demonstrates the exact functionality we just built. Watch how the increase and decrease functions are wired up and how the logic is handled in the reducer.

Watch from 00:45:40 to 00:51:18. Pay attention to how the dispatch function is called with a type and an ID from the button's onClick handler. Then, see how the reducer uses that information to update the state.

An Important Note on Immutability: You may notice in the video that the reducer logic directly modifies the state (State[index].quantity += 1). While this works, it is considered an anti-pattern in React because it mutates state directly. Our approach using .map() is the recommended, immutable way. It prevents potential bugs and works more predictably with React's rendering cycle. Recognizing this difference is a key step in becoming a more proficient React developer.


Conclusion

Congratulations! You've successfully made your shopping cart interactive. Users can now adjust quantities, and the application state updates correctly and efficiently.

Key Takeaways:

  • Complex state logic like updating an item in an array is a perfect use case for useReducer.
  • Reducer logic should always be immutable: create a new state object/array instead of modifying the existing one. The .map() method is your best friend for this.
  • The UI's only job is to dispatch an action describing what happened. The reducer handles all the implementation details. This separation of concerns is key to scalable state management.

Preview of the Next Lesson:

The cart is almost complete! The final piece of core functionality is allowing users to remove items they no longer want. In the next lesson, we will implement functionality to remove an item from the cart, which will follow the same pattern we mastered today: define an action, update the reducer, and dispatch from the UI.