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:
- Import the
useCarthook. - Call the hook to get access to the
dispatchfunction. - Create an event handler function (e.g.,
handleAddToCart). - Inside the handler, call
dispatchwith the correct actiontypeand theproductobject as thepayload. - Attach this handler to an "Add to Cart" button's
onClickevent.
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.
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
useCartcustom hook provides a clean and simple way for any component to access the cart'sstateanddispatchfunction. - To modify the cart, you
dispatchan action object (e.g.,{ type: 'ADD_ITEM', payload: product }) which is handled by thecartReducer. - 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.jsxto 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.