Implementing Product Details, Shopping Cart Logic, and Infinite Scroll in React

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>
    );
  }
}

Tags: React React Router E-commerce javascript web development

Posted on Fri, 08 May 2026 04:53:04 +0000 by gtibok