Product Detail Page Routing and Navigation
To implement a product detail view, you first need to configure the routing structure. The entry point defines the base path, which delegates to a specific layout component handling the internal routing for that section.
// Entry index.js configuration
import ProductLayout from '@/layouts/ProductLayout';
<Route path="/products">
<ProductLayout />
</Route>
The layout component utilizes a Switch to render specific views based on the child path. Here, we define the route for the detail view, accepting a dynamic productId parameter.
// layouts/ProductLayout.js
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import ProductDetail from '@/views/ProductDetail';
export default function ProductLayout() {
return (
<Switch>
<Route path="/products/detail/:productId" component={ProductDetail} />
</Switch>
);
}
Fetching and Displaying Product Data
The ProductDetail component retrieves the productId from the URL parameters to fetch specific data.
import React, { Component } from 'react';
import { fetchProductDetails } from '@/utils/api';
class ProductDetail extends Component {
constructor(props) {
super(props);
this.state = {
productData: null,
isLoading: true
};
}
componentDidMount() {
const { productId } = this.props.match.params;
fetchProductDetails(productId).then(response => {
this.setState({
productData: response.data,
isLoading: false
});
});
}
render() {
const { productData, isLoading } = this.state;
if (isLoading) return <div>Loading...</div>;
return (
<div className="detail-container">
<header className="detail-header">Product Information</header>
<div className="detail-body">
<img src={productData.imageUrl} alt={productData.name} />
<h2>{productData.name}</h2>
<p>{productData.description}</p>
</div>
<footer className="detail-footer">
<button className="add-to-cart-btn">Add to Cart</button>
</footer>
</div>
);
}
}
export default ProductDetail;
Navigation Strategies: Declarative vs. Imperative
You can navigate to the detail page using the Link component or programmatically via the history object.
Declarative Navigation (Link):
import { Link } from 'react-router-dom';
// Inside ProductList component
<Link to={`/products/detail/${item.id}`} className="product-card" key={item.id}>
<img src={item.image} alt={item.name} />
<h3>{item.name}</h3>
</Link>
Imperative Navigation (History Push):
When passing history to a child component:
// Parent Component passes history
<ProductList products={this.state.products} history={this.props.history} />
// Child Component handles click
<li onClick={() => this.props.history.push(`/products/detail/${item.id}`)}>
{/* Content */}
</li>
Shopping Cart Functionality
To manage cart items, specific API endpoints are required to add, update, and remove products.
Adding Items to Cart
// utils/api.js
const addToCart = (userId, productId, quantity) => {
return request.post('/cart/add', { userId, productId, quantity });
};
Implementation in the Detail view:
handleAddToCart = () => {
const userId = localStorage.getItem('uid');
const productId = this.state.productData.id;
const qty = 1;
addToCart(userId, productId, qty).then(res => {
if (res.data.code === '401') {
alert('Please login first');
this.props.history.push('/login');
} else {
alert('Added to cart successfully');
}
});
};
Retrieving and Managing Cart Data
The cart component fetches the user's items, calculates totals, and handles quantity adjustments.
import React, { Component } from 'react';
import { getCartItems, updateCartItem, removeCartItem } from '@/utils/api';
class CartView extends Component {
constructor(props) {
super(props);
this.state = {
items: [],
isEmpty: true,
totalQuantity: 0,
totalPrice: 0,
selectAll: false
};
}
componentDidMount() {
this.loadCartData();
}
loadCartData = () => {
const uid = localStorage.getItem('uid');
getCartItems(uid).then(res => {
if (res.data.code === '401') {
this.props.history.push('/login');
} else if (res.data.data.length > 0) {
const itemsWithSelection = res.data.data.map(i => ({ ...i, selected: true }));
this.setState({ items: itemsWithSelection, isEmpty: false }, this.calculateTotals);
}
});
};
calculateTotals = () => {
let qty = 0;
let price = 0;
this.state.items.forEach(item => {
if (item.selected) {
qty += item.quantity;
price += item.quantity * item.price;
}
});
this.setState({ totalQuantity: qty, totalPrice: price });
};
updateQuantity = (index, change) => {
const updatedItems = [...this.state.items];
const targetItem = updatedItems[index];
const newQty = targetItem.quantity + change;
if (newQty < 1) return;
updateCartItem(targetItem.cartId, newQty).then(res => {
if (res.data.success) {
updatedItems[index].quantity = newQty;
this.setState({ items: updatedItems }, this.calculateTotals);
}
});
};
removeItem = (index) => {
const targetItem = this.state.items[index];
removeCartItem(targetItem.productId).then(res => {
if (res.data.success) {
const newItems = this.state.items.filter((_, i) => i !== index);
this.setState({ items: newItems, isEmpty: newItems.length === 0 }, this.calculateTotals);
}
});
};
toggleSelect = (index) => {
const items = [...this.state.items];
items[index].selected = !items[index].selected;
const allSelected = items.every(i => i.selected);
this.setState({ items, selectAll: allSelected }, this.calculateTotals);
};
toggleSelectAll = () => {
const newState = !this.state.selectAll;
const items = this.state.items.map(i => ({ ...i, selected: newState }));
this.setState({ items, selectAll: newState }, this.calculateTotals);
};
render() {
const { items, isEmpty, totalQuantity, totalPrice, selectAll } = this.state;
return (
<div className="cart-wrapper">
<header>Shopping Cart</header>
<div className="cart-content">
{isEmpty ? (
<p>Your cart is empty.</p>
) : (
<ul>
{items.map((item, idx) => (
<li key={item.cartId}>
<input
type="checkbox"
checked={item.selected}
onChange={() => this.toggleSelect(idx)}
/>
<img src={item.image} alt="product" />
<span>{item.name} - ${item.price}</span>
<button onClick={() => this.updateQuantity(idx, -1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => this.updateQuantity(idx, 1)}>+</button>
<button onClick={() => this.removeItem(idx)}>Remove</button>
</li>
))}
</ul>
)}
<div className="cart-summary">
<label>
<input type="checkbox" checked={selectAll} onChange={this.toggleSelectAll} />
Select All
</label>
<p>Total Quantity: {totalQuantity}</p>
<p>Total Price: ${totalPrice}</p>
</div>
</div>
</div>
);
}
}
Implementing Infinite Scroll and Pull-to-Refresh
For the homepage product list, we utilize react-pullload to handle pagination and refresh actions.
import ReactPullLoad, { STATS } from "react-pullload";
import "react-pullload/dist/ReactPullLoad.css";
class HomeView extends Component {
constructor(props) {
super(props);
this.state = {
products: [],
hasMore: true,
loaderState: STATS.init,
currentPage: 1
};
}
handleAction = (action) => {
if (action === this.state.loaderState) return;
if (action === STATS.refreshing) {
this.refreshList();
} else if (action === STATS.loading) {
this.loadMoreItems();
} else {
this.setState({ loaderState: action });
}
};
refreshList = () => {
if (this.state.loaderState === STATS.refreshing) return;
this.setState({ loaderState: STATS.refreshing });
fetchProducts(1).then(data => {
this.setState({
products: data,
hasMore: true,
currentPage: 1,
loaderState: STATS.refreshed
});
});
};
loadMoreItems = () => {
if (this.state.loaderState === STATS.loading || !this.state.hasMore) return;
this.setState({ loaderState: STATS.loading });
const nextPage = this.state.currentPage + 1;
fetchProducts(nextPage).then(newItems => {
const hasMoreData = newItems.length > 0;
this.setState({
products: [...this.state.products, ...newItems],
hasMore: hasMoreData,
currentPage: nextPage,
loaderState: STATS.reset
});
});
};
render() {
return (
<div className="home-container">
<ReactPullLoad
downEnough={150}
action={this.state.loaderState}
handleAction={this.handleAction}
hasMore={this.state.hasMore}
distanceBottom={100}
>
<ProductList items={this.state.products} />
</ReactPullLoad>
</div>
);
}
}