Frontend Development

Building Shopping Cart With Reactjs and Redux Part 2

Building Shopping Cart With Reactjs and Redux

In this part we of building a shopping cart with Reactjs we will handle cart display, adding products, removing products and updating product quantities.

 

 

To return to the previous part click on this link.

Cart Actions

The cart has three actions which include:

  • Adding products
  • Removing products
  • Updating product quantity

 

Open src/store/actions/cartActions.js and update it as follows:

export const addToCart = (product) => {

    return {
        type: 'ADD_TO_CART',
        payload: {
            product,
            quantity: 1
        }
    }
};

export const removeFromCart = (productId) => {

    return {
        type: 'REMOVE_FROM_CART',
        payload: {
            productId: productId
        }
    }
};

export const updateCartQuantity = (productId, quantity) => {

  return {
      type: 'UPDATE_CART_QUANTITY',
      payload: {
          productId,
          quantity: quantity
      }
  }
};

Here i have added the action creators for each action mentioned above, each action should return an object with a type and payload. We will the type in the reducer below to update the state.

 

Open src/store/reducers/cartReducer.js and update it as follows:

const initialState = {
  cart: []
};

const cartReducer = (state = initialState, action) => {

    let cart = state.cart;

    switch(action.type) {

        case 'ADD_TO_CART':

            cart.push(action.payload);

            return {
                ...state,
                cart: cart
            };
        case 'UPDATE_CART_QUANTITY':

            let item = cart.find(item => item.product.id == action.payload.productId);

            let newCart = cart.filter(item => item.product.id != action.payload.productId);

            item.quantity = action.payload.quantity;

            newCart.push(item);

            return {
                ...state,
                cart: newCart
            };

        case 'REMOVE_FROM_CART':
            return {
                ...state,
                cart: cart.filter(item => item.product.id != action.payload.productId)
            };
        default:
            return state;
    }
};

export default cartReducer;

The code is straightforward you should be familiar with how reducers work, in the above code i make a check for action type and in return i update the state.The most followed approach is using a switch statement but you can also use if else statement if you like.

Note that when updating the state don’t alter the original state instead make a copy of the state, do your logic and then return the new state.

 

Adding To Cart

Now let’s apply those actions into our components, so we will need to update the below components:

src/components/ProductList.js

import React, { Component } from 'react';
import Product from './Product';
import { connect } from 'react-redux';
import {addToCart} from "../store/actions/cartActions";


class ProductList extends Component
{
    addToCart = (product) => {
        this.props.addToCart(product);
    }

    render() {
        return (
            <div className="container">
                <h2>Product List</h2>
                <br/>
                <div className="row">

                    {
                        this.props.products.map(product => <Product product={product} addToCart={this.addToCart} inCart={this.props.cart.length>0 && this.props.cart.filter(e => e.product.id === product.id).length > 0 } key={product.id} /> )
                    }

                </div>
            </div>
        )
    }
}

const mapStateToProps = (state) => {

    return {
        products: state.product.products,
        cart: state.cart.cart
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        addToCart: (product) => {
            dispatch(addToCart(product));
        }
    }
};


export default connect(mapStateToProps, mapDispatchToProps)(ProductList)

src/components/Product.js

import React, { Component } from 'react';

class Product extends Component
{
    state = {
        inCart: this.props.inCart
    };

    addToCart = (e) => {

        e.preventDefault();

        this.props.addToCart(this.props.product)

        this.setState({
            inCart: true
        })
    }

    render() {

        const { product } = this.props;

        return (
            <div className="col-md-3">
                <figure className="card card-product">
                    <div className="img-wrap">
                        <img className="img-responsive" src={product.image} />
                    </div>
                    <figcaption className="info-wrap">
                        <h4 className="title">{product.title}</h4>
                        <p className="desc">{product.description}</p>
                    </figcaption>
                    <div className="bottom-wrap">

                        {
                            this.state.inCart?(
                                <span className="btn btn-success">Added to cart</span>
                            ) : (
                                <a href="#" onClick={this.addToCart} className="btn btn-sm btn-primary float-right">Add to cart</a>
                            )
                        }

                        <div className="price-wrap h5">
                            <span className="price-new">${product.price}</span>
                        </div>
                    </div>
                </figure>
            </div>
        )
    }
}

export default Product;

I added another function into ProductList.js called mapDispatchToProps() which works the same way as mapStateToProps() but it maps the functions to props to be access able inside the components so we can call them on specific events, here in our case we mapped the addToCart() function when the user click the “add to cart” button. You need to update the export statement like this:

export default connect(mapStateToProps, mapDispatchToProps)(ProductList)

Then i passed the addToCart as a prop to the <Product /> child component so i can execute on the button click. Also i sent another prop called inCart which checks if this product is in cart or not to toggle the button visibility and show a label instead.

 

 

Displaying Cart Items

Let’s display the actual cart items instead of the dummy content, so open src/components/cart/Cart.js and update it as follows:

import React, { Component } from 'react';
import Item from './Item';
import { connect } from 'react-redux';

class Cart extends Component
{
    render() {

        let total = 0;

        this.props.cart.map(item => total += item.product.price * item.quantity);

        const cart  = this.props.cart.length > 0?(
            <div>
                <div className="panel-body">
                    {
                        this.props.cart.map(item => {
                            return (
                                <div key={item.product.id}>
                                    <Item item={item} />
                                    <hr />
                                </div>
                            )
                        })
                    }
                </div>
                <div className="panel-footer">
                    <div className="row text-center">
                        <div className="col-xs-11">
                            <h4 className="text-right">Total <strong>${total.toFixed(3)}</strong></h4>
                        </div>

                    </div>
                </div>
            </div>

        ) : (
            <div className="panel-body">
                <p>Cart is empty</p>
            </div>
        )

        return (
                <div className="container">
                    <div className="row">
                        <div className="col-md-12 col-xs-12">
                            <div className="panel panel-info">
                                <div className="panel-heading">
                                    <div className="panel-title">
                                        <div className="row">
                                            <div className="col-xs-6">
                                                <h5><span className="glyphicon glyphicon-shopping-cart"></span> My Shopping Cart</h5>
                                            </div>

                                        </div>
                                    </div>
                                </div>

                                { cart }

                            </div>
                        </div>
                    </div>
                </div>
        )
    }
}

const mapStateToProps = (state) => {

  return {
      cart: state.cart.cart
  }
};

export default connect(mapStateToProps)(Cart);

src/components/cart/Item.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { updateCartQuantity, removeFromCart } from '../../store/actions/cartActions';


class Item extends Component {

    constructor(props) {
        super(props);

        this.state = {
            quantity: this.props.item.quantity,
            btnVisible: false
        };
    }

    handleChange = (e) => {

        if(e.target.value <= 0) {
            alert("Quantity must be greater than or equal to 1");

            return;
        }

        if(e.target.value > this.props.item.product.amount) {
            alert("You have exceeded the available items of this product!");

            return;
        }

        if(this.state.quantity != e.target.value) {
            this.setState({
                quantity: e.target.value,
                btnVisible: true
            });
        }
    }

    handleSubmit = (e) => {
        e.preventDefault();

        this.props.updateCartQuantity(this.props.item.product.id, this.state.quantity);

        this.setState({
            btnVisible: false
        });
    }

    handleRemove = (e) => {
        this.props.removeFromCart(this.props.item.product.id);
    }

  render() {

      const { item } = this.props;

      return (

          <div className="row">
              <div className="col-xs-2"><img className="img-responsive" src={item.product.image} />
              </div>
              <div className="col-xs-4">
                  <h4 className="product-name"><strong>{item.product.title}</strong></h4>
              </div>
              <div className="col-xs-6">
                  <div className="col-xs-3 text-right">
                      <h6><strong>{ item.product.price } <span className="text-muted">x</span></strong></h6>
                  </div>
                  <form onSubmit={this.handleSubmit}>
                      <div className="col-xs-4">
                          <input type="number" className="form-control input-sm" onChange={this.handleChange} value={this.state.quantity}/>
                      </div>

                      {
                          this.state.btnVisible?(
                              <div className="col-xs-2">
                                  <button type="submit" className="btn btn-info">Update</button>
                              </div>
                          ) : null
                      }

                      <div className="col-xs-2">
                          <button type="button" onClick={this.handleRemove} className="btn btn-link btn-xs">
                              <span className="glyphicon glyphicon-trash"> </span>
                          </button>
                      </div>
                  </form>
              </div>
          </div>
      )
  }
}

const mapDispatchToProps = (dispatch) => {

    return {
        updateCartQuantity: (productId, quantity) => dispatch(updateCartQuantity(productId, quantity)),
        removeFromCart: (productId) => dispatch(removeFromCart(productId))
    }
};

export default connect(null, mapDispatchToProps)(Item);

I have accessed the cart from the store using the same technique we used to display the products from the store.

Using mapStateToProps() function in Cart.js i passed the cart then in the render() function i iterated though the cart items using the map() function passing item as a prop to <Item /> child component. Also i calculated the total price of the cart items to be displayed in the footer of the cart page.

Next In the Item component i modified the render() function to display the item details coming from prop like the title, image and price.

 

Removing Cart Items & Updating Quantity

If you notice in the above code i have included the code for removing cart items and updating item quantity by adding this function:

const mapDispatchToProps = (dispatch) => {

    return {
        updateCartQuantity: (productId, quantity) => dispatch(updateCartQuantity(productId, quantity)),
        removeFromCart: (productId) => dispatch(removeFromCart(productId))
    }
};

export default connect(null, mapDispatchToProps)(Item);

Next i update the component to react when the user click the remove button it will call this function:

handleRemove = (e) => {
        this.props.removeFromCart(this.props.item.product.id);
    }

Also when the user updates the quantity i added the onChange() and onSubmit() events:

handleChange = (e) => {

        if(e.target.value <= 0) {
            alert("Quantity must be greater than or equal to 1");

            return;
        }

        if(e.target.value > this.props.item.product.amount) {
            alert("You have exceeded the available items of this product!");

            return;
        }

        if(this.state.quantity != e.target.value) {
            this.setState({
                quantity: e.target.value,
                btnVisible: true
            });
        }
    }

The state item “btnVisible” used to toggle the button visibility when the user update it should be hidden.

Update Navbar.js

Let’s update the Navbar to show the quantity of items selected and the total price

src/components/Navbar.js

import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import { connect } from 'react-redux';

class Navbar extends Component {

    render() {

        this.props.cartUpdated();

        let total = 0;

        this.props.cart.map(item => total += item.product.price * item.quantity);

        return (

            <nav className="navbar navbar-default">
                <div className="container-fluid">
                    <div className="navbar-header">
                        <NavLink className="navbar-brand" to="/">Shopping cart</NavLink>
                    </div>

                    <div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                        <ul className="nav navbar-nav navbar-right">
                            <li><NavLink to="/my-cart">
                                {
                                    this.props.cart.length > 0 ? (
                                        <span className="label label-info">{ this.props.cart.length } items: (${total.toFixed(2)})</span>
                                    ) : null
                                }
                                <i className="glyphicon glyphicon-shopping-cart"></i> My Cart</NavLink></li>
                        </ul>
                    </div>
                </div>
            </nav>
        )
    }
}

const mapStateToProps = (state) => {

    return {
        cart: state.cart.cart,
        cartUpdated: () => { return true }
    }
};

export default connect(mapStateToProps)(Navbar);

All i have included is the mapStateToProps() which injects two props to the component which is the cart and cartUpdated(). The cartUpdate() is a function that returns true, the purpose of this function when called in the component it makes a refresh to the props because props in react don’t refresh by default so we need to refresh it this way.

 

Source Code

https://bitbucket.org/webmobtuts/reactjs-shopping-cart/src

 

 

 

3.2 6 votes
Article Rating

What's your reaction?

Excited
12
Happy
9
Not Sure
6
Confused
5

You may also like

Subscribe
Notify of
guest

11 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Juan
Juan
5 years ago

On Product.js

when I try to add an Item, I get
error on
this.props.addToCart(this.props.product) is not a function

mohammad
mohammad
4 years ago
Reply to  WebMobTuts

i have same problem

chacoroot
chacoroot
4 years ago

there’s some error with the repo, cuz show me this after login 🙁

We can’t let you see this page

To access this page, you may need to log in with another account. You can also return to the previous page or go back to your dashboard.

Babar Mahmood
Babar Mahmood
4 years ago

Hi All I have fixed this issue “this.props.addToCart(this.props.product) is not a function”
In Product.js class add this on top
import { addToCart } from “../store/actions/cartActions”;

then at bottom add this

const mapDispatchToProps = (dispatch) => {
return { clearCart: () => dispatch({ type: CLEAR_CART }) };
};

export default connect(mapStateToProps, mapDispatchToProps)(CartContainer);

This should fix the issue and also i will recommend you guys to check this tutrial
https://www.youtube.com/watch?v=731Ur2HGRBY&t=1441s

aisha
aisha
4 years ago

Thank you for this wonderful tutorial that made me finally learn and understand react

Linda
3 years ago

You have a mutation in your reducer. You cannot do cart.push(action.payload);. You need to use cart: state.cart.concat(action.payload) or cart: [...state.cart, action.payload].