Skip to main content

Welcome to a Grasp lesson

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

Lesson 80

Crafting Your Shopping Cart Page

Recommended resources

Hello! Welcome back to our e-commerce project.

In our last lesson, we successfully implemented the "Add to Cart" functionality on our product list and detail pages. By dispatching an ADD_ITEM action to our cartReducer, we can now update the global cart state from anywhere in our app.

Today, we'll build the user-facing part of that system. Our goal is to build a dedicated Shopping Cart page that displays all the items in the cart, their quantities, and a calculated total price. This lesson will connect the state management logic we've built to a new UI, making our application feel much more complete.

1. Setting Up the Cart Page and Navigation

Before we can display the cart's contents, we need a page for it to live on and a way for users to get there.

Step 1: Create the Page Component

First, create a new file at src/pages/CartPage.jsx and add the basic component structure:

// src/pages/CartPage.jsx
import React from 'react';

const CartPage = () => {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Your Shopping Cart</h1>
      {/* Cart content will go here */}
      <p>Your cart is currently empty.</p>
    </div>
  );
};

export default CartPage;

Step 2: Add the Route

Now, let's make this page accessible via a URL. Open src/App.jsx, import CartPage, and add a new route for /cart.

// src/App.jsx
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import ProductList from './pages/ProductList';
import ProductDetail from './pages/ProductDetail';
import CartPage from './pages/CartPage'; // 1. Import CartPage

function App() {
  // ... your existing App component logic ...

  return (
    <div className="App">
      <Header />
      <main className="container mx-auto p-4">
        <Routes>
          <Route path="/" element={<ProductList />} />
          <Route path="/products/:productId" element={<ProductDetail />} />
          <Route path="/cart" element={<CartPage />} /> {/* 2. Add the new route */}
        </Routes>
      </main>
    </div>
  );
}

export default App;

Step 3: Update the Header Link

Finally, let's give users a way to navigate to the cart. We'll update Header.jsx to include a link to our new page. As a bonus, we'll also display the total number of items currently in the cart.

This is a perfect opportunity to use the .reduce() method you learned about in our first module to calculate the total quantity.

// src/components/Header.jsx
import React from 'react';
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext'; // 1. Import useCart

const Header = () => {
  const { state } = useCart(); // 2. Get the cart state

  // 3. Calculate total number of items
  // We sum the quantity of each item in the cart.
  const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);

  return (
    <header className="bg-gray-800 text-white shadow-md">
      <nav className="container mx-auto p-4 flex justify-between items-center">
        <Link to="/" className="text-xl font-bold">
          E-Store
        </Link>
        <ul className="flex items-center space-x-4">
          <li>
            <Link to="/cart" className="hover:text-gray-300">
              {/* 4. Display the total item count */}
              Cart ({totalItems})
            </Link>
          </li>
          {/* Add other links like Login/Profile later */}
        </ul>
      </nav>
    </header>
  );
};

export default Header;

Now, when you run your app and add items to the cart, you should see the number in the header update automatically! Clicking it will take you to your (currently empty-looking) CartPage.


2. Displaying Cart Items

With the navigation in place, let's make CartPage.jsx actually show the items from our CartContext.

The process is simple:

  1. Use our useCart hook to get the cart state.
  2. Check if state.items is empty. If so, show a message.
  3. If it's not empty, use state.items.map() to render a component for each item.
// src/pages/CartPage.jsx
import React from 'react';
import { useCart } from '../context/CartContext'; // 1. Import the hook

const CartPage = () => {
  const { state } = useCart(); // 2. Get the state
  const { items } = state;

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Your Shopping Cart</h1>
      
      {/* 3. Conditional Rendering */}
      {items.length === 0 ? (
        <p>Your cart is currently empty.</p>
      ) : (
        <div className="flex flex-col gap-4">
          {/* 4. Map over items to display them */}
          {items.map(item => (
            <div key={item.id} className="flex items-center border p-4 rounded-lg shadow-sm">
              <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>
              <div className="text-lg">
                <span className="font-semibold">Qty: {item.quantity}</span>
              </div>
              {/* We will add quantity controls and a remove button here in the next lesson */}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default CartPage;

3. Calculating and Displaying the Total Price

Displaying items is great, but users need to know the total cost. The total price is derived data—it's calculated from the state (items) rather than being stored in the state itself.

We'll use .reduce() again for the calculation. To make our component more efficient, we'll wrap this calculation in the useMemo hook. useMemo will "memoize" (cache) the result of the calculation. It will only recalculate the total when its dependency—the items array—changes, preventing unnecessary work on every re-render.

How to Handle State Management in React

To see a complete, production-style implementation of what we are building, let's look at a great example. This article provides a ShoppingCart component that uses useReducer and useMemo to handle everything from displaying items to calculating totals.

Please review the code for the ShoppingCart.jsx component. Pay close attention to these parts: Reading State: Notice how useReducer provides the state. Displaying Items: Observe the use of state.items.map(...) to render the list. Calculating Totals: See how useMemo and .reduce() are combined to calculate subtotal, total, and itemCount. We are implementing a simplified version of this logic right now.

Now, let's add this logic to our CartPage.jsx.

Here is the final code for the component, including the total price calculation with useMemo.

// src/pages/CartPage.jsx
import React, { useMemo } from 'react'; // 1. Import useMemo
import { useCart } from '../context/CartContext';

const CartPage = () => {
  const { state } = useCart();
  const { items } = state;

  // 2. Calculate the total price with useMemo
  const totalPrice = useMemo(() => {
    return items.reduce((total, item) => total + item.price * item.quantity, 0);
  }, [items]); // Dependency array: recalculate only when 'items' changes

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Your Shopping Cart</h1>
      
      {items.length === 0 ? (
        <p>Your cart is currently empty.</p>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
          <div className="md:col-span-2 flex flex-col gap-4">
            {items.map(item => (
              <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>
                <div className="text-lg text-right">
                  <p className="font-semibold">Qty: {item.quantity}</p>
                  <p className="font-bold mt-2">${(item.price * item.quantity).toFixed(2)}</p>
                </div>
              </div>
            ))}
          </div>

          {/* 3. Cart Summary Section */}
          <div className="md:col-span-1">
            <div className="border p-4 rounded-lg shadow-sm bg-white sticky top-4">
              <h2 className="text-2xl font-bold border-b pb-2 mb-4">Order Summary</h2>
              <div className="flex justify-between mb-2">
                <span className="text-gray-600">Subtotal</span>
                <span className="font-semibold">${totalPrice.toFixed(2)}</span>
              </div>
              <div className="flex justify-between mb-4">
                <span className="text-gray-600">Shipping</span>
                <span className="font-semibold">FREE</span>
              </div>
              <div className="flex justify-between font-bold text-xl border-t pt-4">
                <span>Total</span>
                <span>${totalPrice.toFixed(2)}</span>
              </div>
              <button className="w-full bg-blue-500 text-white py-3 rounded-lg mt-6 hover:bg-blue-600 transition-colors">
                Proceed to Checkout
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default CartPage;

With these changes, your shopping cart page is now fully functional in a read-only state. It accurately reflects the data from your global context and presents it in a clean, user-friendly layout.


Conclusion

Excellent work! You've successfully built a key feature of any e-commerce application. This page serves as the central hub for the user's shopping journey.

Key Takeaways:

  • New pages are created as components and integrated into the app using React Router.
  • Any component can subscribe to and display data from a global context (CartContext).
  • The .map() method is essential for rendering dynamic lists of components from an array in state.
  • Derived data, like a total price, should be calculated on-the-fly. The useMemo hook is a powerful tool for optimizing these calculations to improve performance.

Preview of the Next Lesson:

Our cart page looks great, but it's not interactive yet. In the next lesson, we will implement functionality to update item quantities in the cart. This will involve defining a new UPDATE_QUANTITY action, updating our cartReducer to handle it, and dispatching it from new + and - buttons on our cart page.