Skip to main content

Welcome to a Grasp lesson

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

Lesson 79

Building an E-Commerce Cart: List and Detail Page Integration

Recommended resources

Welcome back!

In the last lesson, we put in the foundational work for our shopping cart by setting up a global CartContext using the useReducer hook. This gave us a centralized place to manage our cart's state and logic.

Today, we'll bring that foundation to life. Our goal is to implement 'Add to Cart' functionality from both the product list and detail pages. We will finally connect our UI to the state management system we built, allowing users to add products to their cart. This is where you'll see the power of combining useReducer and the Context API.

1. Recap: The useCart Hook

Remember the custom hook we created at the end of the last lesson?

// src/context/CartContext.jsx
export const useCart = () => {
  const context = useContext(CartContext);
  // ... error handling ...
  return context;
};

This hook is our gateway to the cart's state. When we call useCart() inside a component, it returns the value we provided to our CartContext.Provider: { state, dispatch }.

  • state: The current state of our cart (e.g., { items: [...] }).
  • dispatch: The function we call to send actions to our reducer.

To add an item, we only need the dispatch function. We'll call it with an action object that matches the structure our reducer expects: { type: 'ADD_ITEM', payload: product }.


2. Adding an Item to the Cart: The Pattern

The pattern for adding an item from any component is straightforward:

  1. Import the useCart hook.
  2. Call the hook to get access to the dispatch function.
  3. Create an event handler function (e.g., handleAddToCart).
  4. Inside the handler, call dispatch with the correct action type and the product object as the payload.
  5. Attach this handler to an "Add to Cart" button's onClick event.

Let's watch a short video that demonstrates this exact process.

How to implement cart in pure React? (useReducer and useContext hooks)

This video from Artur Chmaro's channel, while building a cart from scratch, has a perfect segment showing how to dispatch an 'add' action from a product component. It will reinforce the pattern we're about to implement.

Please watch the segment from 00:11:58 to 00:17:00. Focus on how the useDispatchCart hook (which is analogous to our dispatch function) is imported and used within the Product component to fire an action when a button is clicked. Notice the structure of the dispatched object: { type: 'add', item: ... }.


3. Implementation: The ProductCard Component

First, let's add the functionality to the ProductCard components that appear on your main product listing page.

Step 1: Open ProductCard.jsx

Navigate to src/components/ProductCard.jsx. This component receives a product object as a prop.

Step 2: Import and Use the useCart Hook

At the top of the file, import our custom hook. Then, inside the component function, call it to get the dispatch function.

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

// The product prop is passed from the ProductList page
const ProductCard = ({ product }) => {
  const { dispatch } = useCart(); // 2. Get the dispatch function

  // 3. Create the handler function
  const handleAddToCart = () => {
    dispatch({ type: 'ADD_ITEM', payload: product });
    // Optional: Add some feedback, e.g., a console log or a toast notification
    console.log(`${product.title} added to cart!`);
  };

  return (
    <div className="border rounded-lg p-4 shadow-sm">
      <Link to={`/products/${product.id}`}>
        <img src={product.image} alt={product.title} className="w-full h-48 object-contain mb-4" />
        <h3 className="text-lg font-semibold truncate">{product.title}</h3>
      </Link>
      <p className="text-gray-700 my-2">${product.price.toFixed(2)}</p>
      {/* 4. Add the button and attach the handler */}
      <button
        onClick={handleAddToCart}
        className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 transition-colors"
      >
        Add to Cart
      </button>
    </div>
  );
};

export default ProductCard;

With this change, every product card on your home page now has a working "Add to Cart" button.


4. Implementation: The ProductDetail Page

Next, let's do the same for the product detail page. The process is almost identical, demonstrating the reusability of our context logic.

Step 1: Open ProductDetail.jsx

Navigate to src/pages/ProductDetail.jsx. This component fetches its own product data and stores it in a state variable (likely named product).

Step 2: Add the "Add to Cart" Button

Follow the same pattern as before: import useCart, get dispatch, create a handler, and wire it up to a button. The only difference is where the product data comes from (the component's own state).

// src/pages/ProductDetail.jsx
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useCart } from '../context/CartContext'; // 1. Import useCart

const ProductDetail = () => {
  const { productId } = useParams();
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  const { dispatch } = useCart(); // 2. Get the dispatch function

  useEffect(() => {
    // Your existing fetch logic
    const fetchProduct = async () => {
      try {
        const response = await fetch(`https://fakestoreapi.com/products/${productId}`);
        if (!response.ok) throw new Error('Product not found');
        const data = await response.json();
        setProduct(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchProduct();
  }, [productId]);

  // 3. Create the handler
  const handleAddToCart = () => {
    if (product) {
      dispatch({ type: 'ADD_ITEM', payload: product });
      console.log(`${product.title} added to cart!`);
    }
  };

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!product) return <p>Product not found.</p>;

  return (
    <div className="container mx-auto p-4">
      <div className="flex flex-col md:flex-row gap-8">
        <div className="md:w-1/2">
          <img src={product.image} alt={product.title} className="w-full h-auto object-contain rounded-lg" />
        </div>
        <div className="md:w-1/2">
          <h1 className="text-3xl font-bold mb-4">{product.title}</h1>
          <p className="text-xl text-gray-800 font-semibold mb-4">${product.price.toFixed(2)}</p>
          <p className="text-gray-600 mb-6">{product.description}</p>
          
          {/* 4. Add the button and attach the handler */}
          <button 
            onClick={handleAddToCart}
            className="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition-colors"
          >
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  );
};

export default ProductDetail;

5. Verifying the Result

You've added the buttons, but how can you be sure the state is updating? Right now, the only feedback is the console.log message.

Let's add a temporary log in your main App.jsx to watch the global cart state change in real-time. This is a great way to debug and confirm that the context is working as expected.

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

import { useCart } from './context/CartContext'; // Import useCart

function App() {
  const { state } = useCart(); // Get the state from the cart context

  // Log the cart items whenever they change
  useEffect(() => {
    console.log("Cart state updated:", state.items);
  }, [state.items]);

  return (
    <div className="App">
      <Header />
      <main className="container mx-auto p-4">
        <Routes>
          <Route path="/" element={<ProductList />} />
          <Route path="/products/:productId" element={<ProductDetail />} />
          {/* Future routes will go here */}
        </Routes>
      </main>
    </div>
  );
}

export default App;

Now, open your browser's developer console. As you click the "Add to Cart" buttons on either the product list or a detail page, you will see the Cart state updated: log, showing the array of items in your cart growing and quantities updating.

GitHub Repository: React Shopping Cart with Context and Reducer
This demonstrates a common project structure for building exactly what we're working on: a shopping cart powered by React's Context and Reducer hooks.

Conclusion

Fantastic work! You have successfully connected your React components to the global cart state. Any component in your application can now add items to the cart with just a few lines of code.

Key Takeaways:

  • The useCart custom hook provides a clean and simple way for any component to access the cart's state and dispatch function.
  • To modify the cart, you dispatch an action object (e.g., { type: 'ADD_ITEM', payload: product }) which is handled by the cartReducer.
  • This pattern is highly reusable and works identically across different components (ProductCard, ProductDetail), decoupling the UI from the state logic.
  • You can temporarily log state in a top-level component like App.jsx to easily debug and verify context updates.

Preview of the Next Lesson:

Clicking "Add to Cart" and seeing a console log is great, but users need to see what's in their cart. In the next lesson, we will build a dedicated Shopping Cart page that displays all the items, their quantities, and the total price by reading the data from our global CartContext.