No Time Dad

A blog about web development written by a busy dad

Simple eCommerce Checkout Cart with React

Introducing the worlds most basic eCommerce checkout cart. It’s basic in the sense that it lacks features. It does add and remove items quite well. The checkout cart being basic is intentional. This example is meant to be a lesson on sharing and updating state between react components.

Knowing how to pass state from a parent component to a child component is an important skill for any React developer. The next level skill is knowing how and when to update a parent component’s state from the child component. With that being said, this blog post isn’t meant to be a react useState tutorial or a props tutorial. It’s an example of how to think about component architecture.

This example is going to be made up of three components. A product page component, product list component, and a checkout component. The product list and checkout components are going to be siblings, children of the product page component.

<ProductPage>
  <CheckoutCart />
  <ProductList />
</ProductPage>

Why make the CheckoutCart and ProductList components siblings? Well, they certainly could be and that would make things easier in the moment. But, when you think about it, the ProductList component probably doesn’t care or need to know anything about the state of the CheckoutCart component. Maybe I’d want a flag or message in the product list that a given product is in the cart already, but I’m not sure how common that is.

Keeping CheckingCart and ProductList as siblings prevents them from being tightly coupled to each other. It also allows both components to potentially be stateless, relying only on the props passed to them. Much like attacking the four center squares in chess, a good strategy for getting to clean architecture in React is to strive for stateless components. Just don’t get too hung up on it.

It’s tempting to want to use React’s useContext hook to share state between components but there are a few good reasons not to do that. The first, and maybe most important, is that it’s easy for the global context to become a graveyard of random state values. I’ve seen it happen many times. No one knows why or how that state value got there, but it’s there now and it has to be dealt with for what feels like forever.

So, what should go in useContext? Historically, I’ve made an effort to only use React’s useContext hook for truly global things like theme settings (light/dark), and user authentication status. But, I don’t always stick to that.

Building the components

As mentioned above, the ecommerce checkout cart will consist of three components. I’ll put each of them into their own files, which is usually inside of a components/ directory in my React application. In that directly there can be child directories for each component, but not always. It depends on the project and how much of a hurry I’m in.

This example is going to be mostly unstyled. I do have some default global styles applied on this blog that might show up, but you’ll see that the components are very plain and do not directly import any stylesheets directly. This is intentional. I want to focus on the function of the components before I worry about styling them. Often times I find that these two things compete with each other, and somehow styling seems to win.

A quick reminder of the structure of the components:

<ProductPage>
  <CheckoutCart />
  <ProductList />
</ProductPage>
Parent component

The CheckoutCart and the ProductList are two separate components, but they need to share some of the same state. In fact, the ProductList component needs to be able to update the products in the CheckoutCart component.

An easy way to have this kind of interaction between sibling components is to initialize their state values in a parent component. I can then pass down the shared values and functions as props. Again, this is just one of many approaches but I think it’s the most extendible and relatively easy to understand.

In the ProductPage component I’m create two instances of state using React’s useState hook. One for the products to be listed in the ProductList compoonent, and one for the products in the cart. I could’ve combined them into a single state item, but I think it’s easier to manage them separately. Also, they’re similar but not exactly the same so I think conceptually it’s easier to understand if they’re not merged together.

The other two important things in this parent ProductPage component are the functions that update the state. I won’t go into too much detail on how they work, but the basic idea is that they’re updating the state without modifying it directly. Preserving immutibility. The addToCart function is passed to the ProductList component and the removeFromCart function is passed to the CheckoutCart component. Both of them are later attached to button click handlers.

// product-page.jsx
import React, { useState } from 'react';
import { CheckoutCart } from './checkout-cart.jsx';
import { ProductList } from './product-list';

// This data would typically come from a fetch to a backend somewhere
const PRODUCT_DATA = [
  { id: 1, name: 'Shovel', price: 49.99 },
  { id: 2, name: 'Hammer', price: 24.99 },
  { id: 3, name: 'Drill', price: 199.99 },
];

const ProductPage = () => {

  const [products] = useState(PRODUCT_DATA);
  const [cartProducts, setCartProducts] = useState([]);

  const addToCart = (product) => {
    setCartProducts(oldCart => [...oldCart, product]);
  }

  const removeFromCart = (index) => {
    setCartProducts(oldCart => [...oldCart.slice(0, index), ...oldCart.slice(index + 1)]);
  }

  return (
    <>
      <CheckoutCart cartProducts={cartProducts} removeFromCart={removeFromCart}/>
      <ProductList products={products} addToCart={addToCart} />
    </>
  )

}

export default ProductPage;
Child components

As I mentioned earlier, the child components are stateless. Which makes them flexible and easy to test. The CheckoutCart component’s job is to list items added to the cart and allow users to remove items from the cart. The main workhorse of this component is the “Remove from cart” button. Clicking on this button updates the cartProducts state in the parent component.

The nice thing about this design is that I can easily add some indication of whether a given product is already in the cart or not on the ProductList component. I’d just need to pass the cartProducts state to the component and perform my check.

This version of a checkout cart isn’t great. Well, code-wise it’s fine. But, functionality-wise it’s lacking some features. Right now it just lists all the items and doesn’t keep a count. It also doesn’t total the values or do any of the other things you might expect a checkout cart to do. And that’s okay. The point here is to get a working prototype that can be built on later.

// checkout-card.jsx
import React from 'react';

const CheckoutCart = ({ cartProducts, removeFromCart }) => (
  <>
    Cart
    <ul>
      {cartProducts.map((product, index) =>
        <li key={index}>{product.name} {product.price} <button type="button" onClick={() => removeFromCart(index)}>Remove from cart</button></li>
      )}
    </ul>
    {!cartProducts.length && <span>No products in cart.</span>}
  </>
);

export default CheckoutCart;

The ProductList component is similar to the CheckoutCart component. So similar in fact, that it’s tempting to refactor them into a single component. But, I won’t. The reason I won’t is because both of these components are simple right now, but they probably won’t stay that way for long. New features will be added and eventually there will be less similarities.

Similar to the CheckoutCart component, the “Add to cart” button will modify the parent ProductPage state values for cartProducts, which will in turn update the products displayed in the checkout cart.

// product-list.jsx
import React from 'react';


const ProductList = ({ products, addToCart }) => (
  <>
    Products
    <ul>
      {products.map((product, index) =>
        <li key={index}>{product.name} {product.price} <button type="button" onClick={() => addToCart(product)}>Add to cart</button></li>
      )}
    </ul>
  </>
);

export default ProductList;

Demo, & final code

And there it is. The worlds most basic eCommerce checkout cart with react. Intentionally basic, and ready to be extended and styled.

Demo

Try clicking the “Add to cart” buttons, then clicking the “Remove from cart” buttons to see how the component behaves.

Cart
    No products in cart.Products
    • Shovel 49.99
    • Hammer 24.99
    • Drill 199.99
    Final code
    // product-page.jsx
    import React, { useState } from 'react';
    import { CheckoutCart } from './checkout-cart.jsx';
    import { ProductList } from './product-list';
    
    // This data would typically come from a fetch to a backend somewhere
    const PRODUCT_DATA = [
      { id: 1, name: 'Shovel', price: 49.99 },
      { id: 2, name: 'Hammer', price: 24.99 },
      { id: 3, name: 'Drill', price: 199.99 },
    ];
    
    const ProductPage = () => {
    
      const [products] = useState(PRODUCT_DATA);
      const [cartProducts, setCartProducts] = useState([]);
    
      const addToCart = (product) => {
        setCartProducts(oldCart => [...oldCart, product]);
      }
    
      const removeFromCart = (index) => {
        setCartProducts(oldCart => [...oldCart.slice(0, index), ...oldCart.slice(index + 1)]);
      }
    
      return (
        <>
          <CheckoutCart cartProducts={cartProducts} removeFromCart={removeFromCart}/>
          <ProductList products={products} addToCart={addToCart} />
        </>
      )
    
    }
    
    export default ProductPage;
    // checkout-card.jsx
    import React from 'react';
    
    const CheckoutCart = ({ cartProducts, removeFromCart }) => (
      <>
        Cart
        <ul>
          {cartProducts.map((product, index) =>
            <li key={index}>{product.name} {product.price} <button type="button" onClick={() => removeFromCart(index)}>Remove from cart</button></li>
          )}
        </ul>
        {!cartProducts.length && <span>No products in cart.</span>}
      </>
    );
    
    export default CheckoutCart;
    // product-list.jsx
    import React from 'react';
    
    
    const ProductList = ({ products, addToCart }) => (
      <>
        Products
        <ul>
          {products.map((product, index) =>
            <li key={index}>{product.name} {product.price} <button type="button" onClick={() => addToCart(product)}>Add to cart</button></li>
          )}
        </ul>
      </>
    );
    
    export default ProductList;