Project Overview
In this weekend project, you'll apply your understanding of Redux to build a fully-functional e-commerce shopping cart system. This project brings together everything you've learned this week about Redux principles, state management, actions, reducers, React-Redux integration, middleware, Redux Toolkit, and advanced patterns. By the end of this project, you'll have a solid understanding of how Redux manages state in a real-world application and how it makes complex state interactions more predictable and maintainable.
Learning Objectives
- Implement a Redux store in a React application with proper structure
- Design normalized state shape for efficient data handling
- Create asynchronous actions with Redux Thunk for API interactions
- Optimize performance with selective rendering and memoization
- Build a practical e-commerce interface with complex state management
- Apply proper error handling and loading states
- Implement persistent cart functionality with localStorage
George Polya's 4-Step Problem Solving Method
Step 1: Understand the Problem
Let's break down what we need to build:
- A product listing page that fetches products from an API
- A shopping cart that allows adding/removing products
- Quantity adjustments for cart items
- Cart total calculation
- Persistent cart (remains when page refreshes)
- Loading states and error handling
Step 2: Devise a Plan
Let's create a step-by-step plan to tackle this project:
- Set up project structure with create-react-app and Redux dependencies
- Design the Redux store structure and normalize state
- Create action types, action creators, and reducers
- Set up Redux Thunk for asynchronous API calls
- Build UI components (ProductList, ProductCard, Cart, CartItem, CartSummary)
- Connect components to Redux
- Implement persistent cart with localStorage middleware
- Add loading states and error handling
- Style the application and ensure responsive design
- Test functionality and fix any bugs
Step 3: Execute the Plan
Now, let's start implementing the plan:
Project Setup
First, let's create our React application and install the necessary dependencies:
npx create-react-app redux-shopping-cart
cd redux-shopping-cart
npm install redux react-redux @reduxjs/toolkit axios
Project Structure
Let's set up a clean project structure to organize our code:
src/
├── components/ # React components
│ ├── Cart/
│ ├── Products/
│ └── UI/
├── features/ # Redux slices (using Redux Toolkit)
│ ├── cart/
│ ├── products/
│ └── ui/
├── services/ # API services
├── store/ # Redux store setup
├── utils/ # Helper functions
├── App.js
└── index.js
1. Redux Store Design
Let's design our Redux store with a normalized state shape for efficiency:
Setting Up the Redux Store
Create a Redux store with Redux Toolkit:
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import productsReducer from '../features/products/productsSlice';
import cartReducer from '../features/cart/cartSlice';
import uiReducer from '../features/ui/uiSlice';
// Load cart state from localStorage
const loadCartState = () => {
try {
const serializedState = localStorage.getItem('cart');
if (serializedState === null) {
return undefined;
}
return JSON.parse(serializedState);
} catch (err) {
console.error('Error loading cart state:', err);
return undefined;
}
};
// Save cart state to localStorage
const saveCartState = (state) => {
try {
const serializedState = JSON.stringify(state);
localStorage.setItem('cart', serializedState);
} catch (err) {
console.error('Error saving cart state:', err);
}
};
// Create the Redux store
const store = configureStore({
reducer: {
products: productsReducer,
cart: cartReducer,
ui: uiReducer,
},
preloadedState: {
cart: loadCartState(),
},
});
// Subscribe to store changes to save cart state
store.subscribe(() => {
const state = store.getState();
saveCartState(state.cart);
});
export default store;
2. Products Feature
Let's create our products slice with Redux Toolkit:
// src/features/products/productsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
// Async thunk for fetching products
export const fetchProducts = createAsyncThunk(
'products/fetchProducts',
async (_, { rejectWithValue }) => {
try {
// Using a free API for demo products
const response = await axios.get('https://fakestoreapi.com/products');
return response.data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// Initial state with normalized structure
const initialState = {
productsById: {},
allProductIds: [],
status: 'idle', // idle, loading, succeeded, failed
error: null,
};
// Products slice
const productsSlice = createSlice({
name: 'products',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.status = 'succeeded';
// Normalize the data
state.allProductIds = action.payload.map(product => product.id);
action.payload.forEach(product => {
state.productsById[product.id] = product;
});
})
.addCase(fetchProducts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
});
},
});
// Selectors
export const selectAllProducts = (state) => {
return state.products.allProductIds.map(id => state.products.productsById[id]);
};
export const selectProductById = (state, productId) => {
return state.products.productsById[productId];
};
export const selectProductsStatus = (state) => state.products.status;
export const selectProductsError = (state) => state.products.error;
export default productsSlice.reducer;
3. Cart Feature
Now, let's implement our cart slice:
// src/features/cart/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: {}, // Object with product IDs as keys
itemCount: 0,
totalAmount: 0,
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addToCart: (state, action) => {
const { id, price } = action.payload;
// If item is already in cart, increase quantity
if (state.items[id]) {
state.items[id].quantity += 1;
} else {
// If item is not in cart, add it
state.items[id] = {
...action.payload,
quantity: 1,
};
}
// Update cart summary
state.itemCount += 1;
state.totalAmount += price;
},
removeFromCart: (state, action) => {
const id = action.payload;
const { price, quantity } = state.items[id];
// Update cart summary
state.itemCount -= quantity;
state.totalAmount -= price * quantity;
// Remove the item
delete state.items[id];
},
updateQuantity: (state, action) => {
const { id, quantity } = action.payload;
const currentQuantity = state.items[id].quantity;
const price = state.items[id].price;
// Update cart summary
state.itemCount += quantity - currentQuantity;
state.totalAmount += price * (quantity - currentQuantity);
// Update item quantity or remove if quantity is 0
if (quantity <= 0) {
delete state.items[id];
} else {
state.items[id].quantity = quantity;
}
},
clearCart: (state) => {
return initialState;
},
},
});
// Export actions
export const { addToCart, removeFromCart, updateQuantity, clearCart } = cartSlice.actions;
// Selectors
export const selectCartItems = (state) => {
// Convert object to array for easier rendering
return Object.values(state.items);
};
export const selectCartItemCount = (state) => state.cart.itemCount;
export const selectCartTotal = (state) => state.cart.totalAmount;
export const selectItemQuantity = (state, id) => state.cart.items[id]?.quantity || 0;
export default cartSlice.reducer;
4. UI Components
Let's create our React components for products and cart:
Product List Component
// src/components/Products/ProductList.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchProducts, selectAllProducts, selectProductsStatus, selectProductsError } from '../../features/products/productsSlice';
import ProductCard from './ProductCard';
const ProductList = () => {
const dispatch = useDispatch();
const products = useSelector(selectAllProducts);
const status = useSelector(selectProductsStatus);
const error = useSelector(selectProductsError);
useEffect(() => {
// Only fetch products if we haven't loaded them yet
if (status === 'idle') {
dispatch(fetchProducts());
}
}, [status, dispatch]);
if (status === 'loading') {
return <div className="loading">Loading products...</div>;
}
if (status === 'failed') {
return <div className="error">Error: {error}</div>;
}
return (
<div className="product-list">
<h2>Products</h2>
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
};
export default ProductList;
Product Card Component
// src/components/Products/ProductCard.js
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { addToCart, selectItemQuantity } from '../../features/cart/cartSlice';
const ProductCard = ({ product }) => {
const dispatch = useDispatch();
const quantity = useSelector(state => selectItemQuantity(state, product.id));
const handleAddToCart = () => {
dispatch(addToCart(product));
};
return (
<div className="product-card">
<img src={product.image} alt={product.title} className="product-image" />
<h3 className="product-title">{product.title}</h3>
<p className="product-price">${product.price.toFixed(2)}</p>
<p className="product-category">{product.category}</p>
<button
className="add-to-cart-btn"
onClick={handleAddToCart}
>
Add to Cart {quantity > 0 && `(${quantity})`}
</button>
</div>
);
};
export default React.memo(ProductCard);
Shopping Cart Component
// src/components/Cart/ShoppingCart.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectCartItems, selectCartTotal, clearCart } from '../../features/cart/cartSlice';
import CartItem from './CartItem';
const ShoppingCart = () => {
const dispatch = useDispatch();
const cartItems = useSelector(selectCartItems);
const totalAmount = useSelector(selectCartTotal);
const handleClearCart = () => {
if (window.confirm('Are you sure you want to clear your cart?')) {
dispatch(clearCart());
}
};
if (cartItems.length === 0) {
return (
<div className="shopping-cart">
<h2>Your Cart</h2>
<p>Your cart is empty</p>
</div>
);
}
return (
<div className="shopping-cart">
<h2>Your Cart</h2>
<div className="cart-items">
{cartItems.map(item => (
<CartItem key={item.id} item={item} />
))}
</div>
<div className="cart-summary">
<div className="cart-total">
<h3>Total: ${totalAmount.toFixed(2)}</h3>
</div>
<div className="cart-actions">
<button className="clear-cart-btn" onClick={handleClearCart}>
Clear Cart
</button>
<button className="checkout-btn">
Checkout
</button>
</div>
</div>
</div>
);
};
export default ShoppingCart;
Cart Item Component
// src/components/Cart/CartItem.js
import React from 'react';
import { useDispatch } from 'react-redux';
import { updateQuantity, removeFromCart } from '../../features/cart/cartSlice';
const CartItem = ({ item }) => {
const dispatch = useDispatch();
const handleQuantityChange = (e) => {
const quantity = parseInt(e.target.value, 10);
dispatch(updateQuantity({ id: item.id, quantity }));
};
const handleRemove = () => {
dispatch(removeFromCart(item.id));
};
return (
<div className="cart-item">
<img
src={item.image}
alt={item.title}
className="cart-item-image"
/>
<div className="cart-item-details">
<h4 className="cart-item-title">{item.title}</h4>
<p className="cart-item-price">${item.price.toFixed(2)}</p>
</div>
<div className="cart-item-actions">
<div className="quantity-control">
<button
onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity - 1 }))}
disabled={item.quantity <= 1}
>
-
</button>
<input
type="number"
value={item.quantity}
onChange={handleQuantityChange}
min="1"
/>
<button
onClick={() => dispatch(updateQuantity({ id: item.id, quantity: item.quantity + 1 }))}
>
+
</button>
</div>
<button className="remove-btn" onClick={handleRemove}>
Remove
</button>
</div>
<div className="cart-item-subtotal">
${(item.price * item.quantity).toFixed(2)}
</div>
</div>
);
};
export default React.memo(CartItem);
App Component
// src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import ProductList from './components/Products/ProductList';
import ShoppingCart from './components/Cart/ShoppingCart';
import './App.css';
function App() {
return (
<Provider store={store}>
<div className="app">
<header className="app-header">
<h1>Redux Shopping Cart</h1>
</header>
<main className="app-main">
<div className="container">
<div className="row">
<div className="col-products">
<ProductList />
</div>
<div className="col-cart">
<ShoppingCart />
</div>
</div>
</div>
</main>
</div>
</Provider>
);
}
export default App;
Understanding Redux Flow in the Shopping Cart
How Redux Manages Shopping Cart State
Let's break down how Redux manages the shopping cart state in our application:
- User Action: When a user clicks "Add to Cart", a Redux action is dispatched.
- Action Processing: The action goes through any middleware (like Redux Thunk for async operations).
- Reducer Update: The cart reducer processes the action and updates the state accordingly.
- State Changes: The Redux store is updated with the new state.
- Persistence: Our store subscriber saves the cart state to localStorage.
- UI Updates: Components connected to the store via useSelector re-render with the new data.
This flow ensures that our shopping cart state is:
- Predictable: Each action follows the same path through the Redux architecture
- Maintainable: State logic is separated from UI components
- Debuggable: Each state change can be tracked with Redux DevTools
- Persistent: Cart data survives page refreshes through localStorage
Real-World Application
E-commerce shopping carts are excellent examples of complex state management scenarios:
- Multiple Data Sources: Product data from APIs, user cart data from local state
- Complex Calculations: Price totals, quantity adjustments, discounts
- Persistence Requirements: Cart needs to remain when users navigate away or refresh
- Synchronization Challenges: Local cart might need to sync with backend data
Major e-commerce platforms like Amazon, Shopify stores, and Walmart all use similar patterns to what we've built, but at a much larger scale with additional features:
- Saved items for later
- Personalized recommendations
- Inventory tracking
- Tax calculations
- Shipping options
- Discount code application
Understanding how to build a Redux-powered shopping cart gives you insight into how these large-scale applications manage their complex state.
Step 4: Review and Reflect
Common Challenges and Solutions
| Challenge | Solution |
|---|---|
| Cart items disappear on refresh | Implement localStorage persistence as shown in our store setup |
| Inefficient re-renders | Use React.memo for components and implement proper selector functions |
| Complex state updates | Leverage Redux Toolkit's immer integration for simpler updates |
| API failures | Implement proper error handling in async thunks |
| Cart synchronization with backend | Add middleware to sync cart changes with your backend API |
Enhancement Ideas
- Add product categories and filtering
- Implement a wishlist feature
- Add product search functionality
- Create a checkout process with address form
- Implement discount code functionality
- Add user authentication and saved carts
Project Requirements
Your weekend project should include:
- Product Listing: Display products fetched from an API
- Shopping Cart: Add/remove products, adjust quantities
- Cart Total: Calculate and display cart total
- Persistence: Cart survives page refreshes
- Redux Integration: Proper Redux setup with actions, reducers, and store
- Error Handling: Handle API failures gracefully
- Responsive Design: Works on mobile and desktop
- Performance Optimization: Memoized components and selectors
Submission Guidelines
- Push your code to a GitHub repository
- Include a README with setup instructions
- Deploy your application to GitHub Pages or Netlify (optional)
- Be prepared to discuss your implementation in the next class
Conclusion
Building a Redux-powered e-commerce shopping cart is an excellent way to solidify your understanding of Redux state management. This project combines many real-world requirements that you'll encounter in professional development:
- Working with APIs and handling asynchronous data
- Managing complex UI state with interdependent parts
- Implementing data persistence
- Creating a responsive and user-friendly interface
- Optimizing performance in React applications
Remember that Redux is just one approach to state management. While it's powerful for complex applications, simpler solutions like React Context or local component state might be more appropriate for smaller projects. The skills you've learned this week will help you make informed decisions about state management in your future React applications.
Happy coding, and don't hesitate to reach out if you have questions or challenges with your implementation!