React.js create cart

Overview

The goal of this guide is to demonstrate how to add cart functionality to your products page so multiple products can be added to a cart, increase or decrease the item quantities, and clear items from the cart.

Below is what you will accomplish with this guide:

  1. Retrieve and/or create a cart in our application
  2. Add products to cart
  3. Update line items in cart
  4. Remove line items from cart
  5. Empty cart contents

See live demo

react cart demo

Requirements

To start this guide you will need:

  • An IDE or code editor
  • NodeJS, at least v8
  • npm or yarn
  • React devtools (recommended)

Prerequisites

This project assumes you have some knowledge of the below concepts before starting:

  • Basic knowledge of JavaScript
  • Some knowledge of React
  • An idea of the Jamstack architecture and how APIs work

Some things to note

  • The purpose of this guide is to focus on the Commerce.js integration and using React to build the application, we will therefore not be covering any styling details

Add cart functionality

The cart resource in Chec comes equipped with multiple intuitive endpoints to help develop a seamless shopping cart experience with Commerce.js. You will be interacting with the cart endpoint in multiple components in your application:

  • In your product listing page where you can add items to the cart
  • In the cart component where you will be rendering, updating, removing, and clearing the cart items

1. Retrieve cart

In the app component, follow the same logic to fetch and retrieve your cart data after the component renders, the same as fetching your products. First let's add a cart state to store the cart data that will be returned under the products state.

// App.js

const [cart, setCart] = useState({});

Next, we will use another Commerce method to retrieve the current cart in session with cart.retrieve(). Commerce.js automatically creates a cart for you if one does not exist in the current browser session. Commerce.js tracks the current cart ID with a cookie, and stores the entire cart and its contents for 30 days. This means that users returning to your website will still have their cart contents available for up to 30 days.

With the Cart API and cart methods in Commerce.js, the otherwise complex cart logic can be easily implemented. Now let's add a new cart method underneath fetchProducts().

/**
 * Retrieve the current cart or create one if one does not exist
 * https://commercejs.com/docs/sdk/cart
 */
const fetchCart = () => {
  commerce.cart.retrieve().then((cart) => {
    setCart(cart);
  }).catch((error) => {
    console.log('There was an error fetching the cart', error);
  });
}

Above, you created a new helper function called fetchCart() that will call the cart.retrieve() method to retrieve the cart in session or create a new one if one does not exist. When this method resolves, use setCart to set the returned cart data object to the cart state. Otherwise, handle a failed request with an error message. And again, we'll want to execute this method in the useEffect React hook to always make sure our most up to date cart data is returned.

// App.js

useEffect(() => {
  fetchProducts();
  fetchCart();
}, []);

The cart.retrieve() method will run, resolve, and the returned data will be stored in the cart state. Fire up your page, and the result should be similar to the cart object response you see in the network request response.

2. Add to cart

The next functionality we will want to add is the ability to add products to a cart. We will be using the method cart.add. which calls the POST v1/carts/{cart_id} Cart API endpoint. With the cart object response, we can start to interact with and add the necessary event handlers to handle cart functionalities. Similar to how you can pass props as custom attributes, you can do that with native and custom events via callbacks. Because we will need to display a button to handle the add to cart functionality, let's go back to the ProductItem.js component to add that in the product card under the price element. Create a button tag and pass a function handleAddToCart to the React native onClick attribute which will be the function handler we will create to handle the event.

<button
  name="Add to cart"
  className="product__btn"
  onClick={handleAddToCart}
>
  Quick add
</button>

In React, data being passed down from a parent component to a child component is called props. In order to listen for any events in a child component, use callback functions. After attaching a click event in your Quick add button to call the handleAddToCart event handler, now create the handler function.

// ProductItem.js
const handleAddToCart = () => {
  onAddToCart(product.id, 1);
}

Inside the handler function handleAddToCart(), execute a callback function which will be passed in from the App.js component via props - onAddToCart. Note that you will get to creating and passing this callback in the next section. A callback can receive any arguments, and the App.js component will have access to them. In this case, pass product.id and the quantity 1 as these are the request parameters for using the commerce.cart.add() method.

Now go to ProductsList.js and attach the callback function to the onAddToCart attribute.

// ProductsList.js

const ProductsList = ({ products, onAddToCart }) =>  (
  <div className="products" id="products">
    {products.map((product) => (
      <ProductItem
        key={product.id}
        product={product}
        onAddToCart={onAddToCart}
      />
    ))}
  </div>
);

Head back to App.js to pass in your callback onAddToCart in the ProductsListing component instance and attach a handleAddToCart() method and make the "add to cart" request to the Chec API.

// App.js

<ProductsList
  products={products}
  onAddToCart={handleAddToCart}
/>

The data product.id and the quantity 1 that were passed in to the callback function in ProductItem component will be received in the handling method. Go ahead and create the helper handling method and call it handleAddToCart() in the App.js component. You will also need to pass in parameters productId and quantity as variables.

/**
 * Adds a product to the current cart in session
 * https://commercejs.com/docs/sdk/cart/#add-to-cart
 *
 * @param {string} productId The ID of the product being added
 * @param {number} quantity The quantity of the product being added
 */
const handleAddToCart = (productId, quantity) => {
  commerce.cart.add(productId, quantity).then((item) => {
    setCart(item.cart);
  }).catch((error) => {
    console.error('There was an error adding the item to the cart', error);
  });
}

The above helper handle makes a call to the commerce.cart.add method. You will also need to pass in parameters productId and quantity as variables for. When the promise resolves, we set the state again by updating the cart with the new cart data.

3. Create a cart component

Start by creating a cart component in the components folder. Here you will want to follow the same pattern to try to encapsulate and break down smaller components to be consumable by parent components. This way you continue to keep your application DRY as well and keep your logic separated.

In your components folder, create a Cart.js as a class component, this will render the main cart container.

import React, { Component } from 'react';
import CartItem from './CartItem';

const Cart = ({ cart }) => {

  const handleEmptyCart = () => {
    onEmptyCart();
  }

  const renderEmptyMessage = () => {
    if (cart.total_unique_items > 0) {
      return;
    }

    return (
      <p className="cart__none">
        You have no items in your shopping cart, start adding some!
      </p>
    );
  }

  const renderItems = () => (
    cart.line_items.map((lineItem) => (
      <CartItem
        item={lineItem}
        key={lineItem.id}
        className="cart__inner"
      />
    ))
  );

  const renderTotal = () => (
    <div className="cart__total">
      <p className="cart__total-title">Subtotal:</p>
      <p className="cart__total-price">{cart.subtotal.formatted_with_symbol}</p>
    </div>
  );

  return (
    <div className="cart">
      <h4 className="cart__heading">Your Shopping Cart</h4>
      { renderEmptyMessage() }
      { renderItems() }
      { renderTotal() }
      <div className="cart__footer">
        <button className="cart__btn-empty">Empty cart</button>
        <button className="cart__btn-checkout">Checkout</button> 
      </div>
    </div>
  );
};

Cart.propTypes = {
    cart: PropTypes.object,
    onEmptyCart: () => {},
};

export default Cart;

In Cart.js, import in a CartItem component you will create next. You've now split up your rendering methods into a couple of parts:

  • Render a message when the cart is empty
  • Render the contents of the cart when it is not empty
  • Return the main component that calls the above two methods

To render an empty cart message, first check that the cart is empty and return early if it isn't. Use the cart.total_unique_items property to determine this. Return a simple paragraph tag with a message in it.

When rendering the cart you'll do the opposite check from renderEmptyCart() checking that the cart does have items in it, returning early if not. Next, you will want to render out the individual line items that exists in the cart object when items are added to cart. You're rendering a CartItem component for each line item, providing the line item object as the item prop, and assigning it a unique key with the line item's id property.

Next, render some cart subtotals. You can use the cart.subtotal.formatted_with_symbol property to get the cart's subtotal with the currency symbol (e.g. $19.95). This property will be updated whenever your cart object changes in the state, so your cart updates in real time!

Finally, we add a handler for empty the cart contents and pass that in to the onClick attribute.

Create the cart item component

Next, create the CartItem.vue class component which will render each line item details such as the item image, name, price, and quantity.

// Cart.vue

import React from 'react';
import PropTypes from 'prop-types';

const CartItem = ({ item }) => {

  return (
    <div className="cart-item">
      <img className="cart-item__image" src={item.image.url} alt={item.name} />
      <div className="cart-item__details">
        <h4 className="cart-item__details-name">{item.name}</h4>
        <div className="cart-item__details-qty">
          <p>{item.quantity}</p>
        </div>
        <div className="cart-item__details-price">{item.line_total.formatted_with_symbol}</div>
      </div>
      <button
        type="button"
        className="cart-item__remove"
      >
        Remove
      </button>
    </div>
  );
};

CartItem.propTypes = {
    item: PropTypes.object,
};

export default CartItem;

For now, build out the JSX template with the item prop to parse item.image.url as the src value, the item.name, the item.quanity and the item.line_total.formatted_with_symbol. Later on, you will be adding events to the buttons above to have the functionality to update and remove each line item.

At this stage, you should be able to see a minimal cart component rendered out in your main application view. Let's continue to add more cart functionality and build a more detailed cart interface.

4. Update cart items

Going back to the CartItem.vue component, you can start to implement the first cart line item action using the Commerce.js method commerce.cart.update(). This request uses the PUT v1/carts/{cart_id}/items/{line_item_id} API to update the quantity or variant for the line item ID in the cart. For this guide, you will only be working with the main variant of the product item.

Add a new handler in the CartItem.vue component to call a callback function onUpdateCartQty. Pass in lineItemId and quantity to this callback function.

// CartItem.vue

const handleUpdateCartQty = (lineItemId, quantity) => {
  onUpdateCartQty(lineItemId, quantity);
}

You've now created a handler function handleUpdateCartQty() to call a onUpdateCartQty() callback property. The parameters passed in will be available to the parent component, your App.js in this case, which will handle and execute the updating of line items in your cart.

Now in your CartItem component, hook up your "update cart quantity" functionality with a click handler. Between the item quantity element, attach your custom handleUpdateCartQty method to button click events. In the first button, implement a click handler to decrease the line item quantity by 1 and in the second button to increase it by 1.

// CartItem.js
<div className="cart-item">
  <img className="cart-item__image" src={item.image.url} alt={item.name} />
  <div className="cart-item__details">
    <h4 className="cart-item__details-name">{item.name}</h4>
    <div className="cart-item__details-qty">
        <button type="button" onClick={() = handleUpdateCartQty(item.id, item.quantity - 1)}>-</button>
        <p>{item.quantity}</p>
        <button type="button" onClick={() => handleUpdateCartQty(item.id, item.quantity + 1)}>+</button>
    </div>
    <div className="cart-item__details-price">{item.line_total.formatted_with_symbol}</div>
  </div>
  <button
    type="button"
    className="cart-item__remove"
  >
    Remove
  </button>
</div>

When the click event fires it will call the handleUpdateCartQty() method with the quantity of the item decreased or increased by 1.

For the App.js component to handle the callback function, create an event handler for the updating of the line item quantities.

// App.vue

/**
 * Updates line_items in cart
 * https://commercejs.com/docs/sdk/cart/#update-cart
 *
 * @param {string} lineItemId ID of the cart line item being updated
 * @param {number} newQuantity New line item quantity to update
 */
const handleUpdateCartQty = (lineItemId, quantity) => {
  commerce.cart.update(lineItemId, { quantity }).then((resp) => {
    setCart(resp.cart);
  }).catch((error) => {
    console.log('There was an error updating the cart items', error);
  });
}

In this helper function, call the commerce.cart.update() endpoint with lineItemId and quantity. When you fire the update button in your CartItem.js component, this event handler will run and will update the state with the new cart object when it resolves.

Next, let's then get to creating a component for the cart header navigation and hook up your callback function as a prop to the cart component instance in this component. Name the file CartNav.js.

const CartNav = ({ cart, onRemoveFromCart }) => {
  const [isCartVisible, setCartVisible] = useState(false);

  const renderOpenButton = () => (
    <button className="nav__cart-btn--open">
      <FontAwesomeIcon size="2x" icon="shopping-bag" color="#292B83"/>
      {cart !== null ? <span>{cart.total_items}</span> : ''}
    </button>
  );

  const renderCloseButton = () => (
    <button className="nav__cart-btn--close">
      <FontAwesomeIcon size="1x" icon="times" color="white"/>
    </button>
  );

  return (
    <div className="nav">
    <div className="nav__cart" onClick={() => setCartVisible(!isCartVisible)}>
        { !isCartVisible ? renderOpenButton() : renderCloseButton() }
    </div>
      { isCartVisible &&
        <Cart
          cart={cart}
          onUpdateCartQty={handleUpdateCartQty}
        />
      }  
    </div>
  );
};

export default CartNav;

Now update your App.js with the CartNav component instance.

<CartNav 
  cart={cart}
  onUpdateCartQty={handleUpdateCartQty}
/>

Go ahead and click an update button for one of the line items. Upon a successful request you will see a successful network request event Cart.Item.Updated fired.

5. Remove items from cart

Now that you have the ability to update the quantity of individual line items in your cart, it's a good idea to let customers remove line items from your cart entirely. The Commerce.js commerce.cart.remove() method helps with this.

Go back to your CartItem.js component to add the "remove item from cart" logic. Underneath handleUpdateCartQty(), add a helper method and call it handleRemoveFromCart().

// CartItem.js

const handleRemoveFromCart = () => {
  onRemoveFromCart(item.id);
}

Once again, this handler method will be the one to call a onRemoveFromCart() callback function which will make the lineItemId data available to the App.js component for which line item is being removed. An updated CartItem.js component with added handlers both bound to the component will look like this:

const handleUpdateCartQty = (lineItemId, quantity) => {
  onUpdateCartQty(lineItemId, quantity);
}

const handleRemoveFromCart = () => {
  onRemoveFromCart(item.id);
}

Attach the handleRemoveFromCart() method to an isolated Remove button as well. When this click handler fires, the associated line item will be removed from the cart object.

// CartItem.vue

return (
  <div className="cart-item">
    <img className="cart-item__image" src={item.image.url} alt={item.name} />
    <div className="cart-item__details">
      <h4 className="cart-item__details-name">{item.name}</h4>
      <div className="cart-item__details-qty">
        <button type="button" onClick={() => item.quantity > 1 ? handleUpdateCartQty(item.id, item.quantity - 1) : handleRemoveFromCart()}>-</button>
        <p>{item.quantity}</p>
        <button type="button" onClick={() => handleUpdateCartQty(item.id, item.quantity + 1)}>+</button>
      </div>
      <div className="cart-item__details-price">{item.line_total.formatted_with_symbol}</div>
    </div>
    <button
      type="button"
      className="cart-item__remove"
      onClick={handleRemoveFromCart}
    >
      Remove
    </button>
  </div>

Finally, in App.js, create the event handler to make the request to the commerce.cart.remove() method. This is the event handler you pass to your CartItem in the onRemoveFromCart prop. The commerce.cart.remove() method takes an lineItemId argument and once the promise resolves, the new cart object has one less of the removed line item (or the item removed entirely if you decrease down to a quantity of zero).

// App.js

/**
 * Removes line item from cart
 * https://commercejs.com/docs/sdk/cart/#remove-from-cart
 *
 * @param {string} lineItemId ID of the line item being removed
 */
const handleRemoveFromCart = (lineItemId) => {
  commerce.cart.remove(lineItemId).then((resp) => {
    setCart(resp.cart);
  }).catch((error) => {
    console.error('There was an error removing the item from the cart', error);
  });
}

Update your App component to provide the onRemoveFromCart prop to the Cart component.

// App.js
  <CartNav 
    cart={cart}
    onUpdateCartQty={handleUpdateCartQty}
    onRemoveFromCart={handleRemoveFromCart
  />

6. Clear cart

Lastly, the cart action to go over in this guide is the commerce.cart.empty() method. The empty() method completely clears the contents of the current cart.

Since removal of the entire cart contents will happen at the cart component level, intercept an event for it directly in the cart UI. Go back to your Cart component and add a click handler which will call handleEmptyCart(). Underneath the component instance of CartItem, add in the button below:

// Cart.js

<button className="cart__btn-empty" onClick={handleEmptyCart}>Empty cart</button>

Now, add a new handler method in the Cart.js component to call the callback prop onEmptyCart that will be passed down from App.js.

// Cart.js

const handleEmptyCart = () => {
  onEmptyCart();
}

In App.js, create an event handler to empty the cart. The commerce.cart.empty() method has no arguments - it simply deletes all the items in the cart.

// App.js

/**
 * Empties cart contents
 * https://commercejs.com/docs/sdk/cart/#remove-from-cart
 */
const handleEmptyCart = () => {
  commerce.cart.empty().then((resp) => {
    setCart(resp.cart);
  }).catch((error) => {
    console.error('There was an error emptying the cart', error);
  });
}

Continue to go up to the parent CartNav.js component to attach the callback.

// CartNav.js

{isCartVisible &&
  <Cart
    cart={cart}
    onUpdateCartQty={onUpdateCartQty}
    onRemoveFromCart={onRemoveFromCart}
    onEmptyCart={onEmptyCart}
  />
}  

You'll need to hook it up to your cart component in your app component as well.

// App.js

return (
  <div className="app">
    <CartNav 
      cart={cart}
      onUpdateCartQty={handleUpdateCartQty}
      onRemoveFromCart={handleRemoveFromCart}
      onEmptyCart={handleEmptyCart}
    />
  </div>
);

That's it!

And there you have it, you have now wrapped up part two of the Commerce.js React guide on implementing cart functionality in your application. The next guide will continue from this to add a checkout flow.

You can find the full finished code in GitHub here!

Edit this page on GitHub