Skip to main content

How to Create an Ecommerce Site with React

By Deven Rathore

JavaScript

Share:

Free JavaScript Book!

Write powerful, clean and maintainable JavaScript.

RRP $11.95

Since most of the performance optimizations for ecommerce web applications are front-end related, the prominent, front-end centric framework React — often preferred for its simplicity and elegance — will be used in this tutorial.

In creating a basic ecommerce site, we’ll also make use of React Context as an alternative to state management frameworks such as Redux and MobX.

Also, a basic method for handling authentication and cart management will be shown in this application. Below is a screenshot of what we’ll be building:

The finished app

This tutorial assumes you have a basic knowledge of JavaScript and JSX.

Prerequisites

In the course of building this application, we’ll be using React Context to manage our application data, because it “provides a way to pass data through the component tree without having to pass props down manually at every level”, as outlines in the docs.

To build the application, you’ll also need a computer with the required applications for starting and running a React application — Node >= 8.10 and npm >= 5.6 on your machine.

Create a React project like so:

npx create-react-app e-commerce

In this application, we also use React Router to handle the routing. To install this module, run this:

npm install react-router-dom

Finally, we’ll use the Bulma open-source CSS framework to style this application. To install this, run the following command:

npm install bulma

Getting Started

First, we need to add the stylesheet to our application. To achieve that, we’ll add an import statement to include this file in the index.js file in the src folder of the application. This will apply the stylesheet across all the components in the application. Below is a snippet of the import statement:

// ./index.js
...
import "bulma/css/bulma.css";
...

In this application, we define a dataset for the application, since we aren’t working with a back-end API. The dataset contains the application’s users and the initial products list:

// ./Data.js
export default {
  users: [
    { username: "regular", accessLevel: 1, password: "password" },
    { username: "admin", accessLevel: 0, password: "password" }
  ],
  initProducts: [
    {
      name: "shoes",
      stock: 10,
      price: 399.99,
      shortDesc: "Nulla facilisi. Curabitur at lacus ac velit ornare lobortis.",
      description:
        "Cras sagittis. Praesent nec nisl a purus blandit viverra. Ut leo. Donec quam          elis, ultricies nec, pellentesque eu, pretium quis, sem. Fusce a quam."
    },
    ...moreProducts
  ]
};

Context Setup

In complex applications where the need for Context is usually necessary, there can be multiple contexts, with each having its data and methods relating to the set of components that requires the data and methods. For example, there can be a ProductContext for handling the components which use products related data, and another ProfileContext for handling data relating to authentication and user data. However, for the sake of keeping things as simple as possible, we’ll use just one context instance.

In order to create the context, we create a file Context.js in the root of our application:

// ./Context.js

import React from "react";
const Context = React.createContext({});
export default Context;

This creates the Context and initializes the context data to an empty object. Next, we need to create a component wrapper, which we’ll use to wrap components that use the Context data and methods:

    // ./withContext.js

import React from "react";
import Context from "./Context";
const withContext = WrappedComponent => {
  const WithHOC = props => {
    return (
      <Context.Consumer>
        {context => <WrappedComponent {...props} context={context} />}
      </Context.Consumer>
    );
  };
  WithHOC.WrappedComponent = WrappedComponent;
  return WithHOC;
};
export default withContext;

The code above exports a function withContext, which uses the Consumer Component property of the previously created Context. The Consumer component transmits data associated with the Context to its child component.

App Development

Next, we need to set up up the App.js file. Here, we’ll be handling the application’s navigation as well as defining its data and methods to manage them.

First, let’s set up up the application navigation:

//./src/App.js

import React, { Component } from "react";
import { Switch, Route, Link, BrowserRouter as Router } from "react-router-dom";
import data from "./Data";
import Context from "./Context";
export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.routerRef = React.createRef();
  }
  ...
  ...
  ...

  render() {
    return (
      <Context.Provider
        value={{
          ...this.state,
          removeFromCart: this.removeFromCart,
          addToCart: this.addToCart,
          login: this.login,
          addProduct: this.addProduct,
          clearCart: this.clearCart,
          checkout: this.checkout
        }}
      >
        <Router ref={this.routerRef}>
          <div className="App">
            <nav
              className="navbar container"
              role="navigation"
              aria-label="main navigation"
            >
              <div className="navbar-brand">
                <b className="navbar-item is-size-4 ">ecommerce</b>
                <a
                  role="button"
                  class="navbar-burger burger"
                  aria-label="menu"
                  aria-expanded="false"
                  data-target="navbarBasicExample"
                  onClick={e => {
                    e.preventDefault();
                    this.setState({ showMenu: !this.state.showMenu });
                  }}
                >
                  <span aria-hidden="true"></span>
                  <span aria-hidden="true"></span>
                  <span aria-hidden="true"></span>
                </a>
              </div>
              <div className={`navbar-menu ${
                  this.state.showMenu ? "is-active" : ""
                }`}>
                <Link to="/products" className="navbar-item">
                  Products
                </Link>
                {this.state.user && this.state.user.accessLevel < 1 && (
                  <Link to="/add-product" className="navbar-item">
                    Add Product
                  </Link>
                )}
                <Link to="/cart" className="navbar-item">
                  Cart
                  <span
                    className="tag is-primary"
                    style={{ marginLeft: "5px" }}
                  >
                    {Object.keys(this.state.cart).length}
                  </span>
                </Link>
                {!this.state.user ? (
                  <Link to="/login" className="navbar-item">
                    Login
                  </Link>
                ) : (
                  <a className="navbar-item" onClick={this.logout}>
                    Logout
                  </a>
                )}
              </div>
            </nav>
            <Switch>
              <Route exact path="/" component={Component} />
              <Route exact path="/login" component={Component} />
              <Route exact path="/cart" component={Component} />
              <Route exact path="/add-product" component={Component} />
              <Route exact path="/products" component={Component} />
            </Switch>
          </div>
        </Router>
      </Context.Provider>
    );
  }
}

Our App component will be responsible for initializing the application data and will also define methods to manipulate these data. Here, we define the context data and methods using the context Provider component. The data and methods are passed as a property, value, on the Provider component to replace the object given on the context creation. (Note that the value can be of any data type.) We pass the state value and some methods, which we’ll define soon.

Next, we build our application navigation. To achieve this, we need to wrap our app with a Router component, which can either be BrowserRouter (like in our case) or HashRouter. Next, we define our application’s routes using the Switch and Route component. We also create the app’s navigation menu, with each link using the Link component provided in the React Router module. We also add a reference, routerRef, to the Router component to enable us to access the router from the App component.

App component

User Authentication

In the next step, we’ll handle the user authentication. First we need to initialize the user in the App component constructor. This is shown below:

//./src/App.js
...  
  constructor(props) {
    super(props);
    this.state = {
      user: null
    };
  }
...

Next, we make sure the user is loaded when the application starts up by setting the user on component mount, as shown below:

//.src/App/.js
...
componentDidMount() {
    let user = localStorage.getItem("user");
    user = user ? JSON.parse(user) : null;
    this.setState({ user });
  }
...

Here, we load the last user session from the local storage to the state if it exists. Next, we define the login and logout methods attached to the Context:

//./src/App.js
...
  login = (usn, pwd) => {
    let user = data.users.find(u => u.username === usn && u.password === pwd);
    if (user) {
      this.setState({ user });
      localStorage.setItem("user", JSON.stringify(user));
      return true;
    }
    return false;
  };
  logout = e => {
    e.preventDefault();
    this.setState({ user: null });
    localStorage.removeItem("user");
  };
...

The login method checks if the user and password combination match any user in the array and sets the state user if found. It also adds the user to local storage for persistency. The logout clears the user from both state and local storage.

Next, we create the login component. First, we create a components folder where we’ll put all our components, then create a Login.js file. This component makes use of the context data. For it to have access to these data and methods, it has to be wrapped using the withContext method we created earlier.

// ./src/components/Login.js

import React, { Component, Fragment } from "react";
import withContext from "../withContext";
import { Redirect } from "react-router-dom";
class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: ""
    };
  }
  handleChange = e =>
    this.setState({ [e.target.name]: e.target.value, error: "" });

  login = () => {
    const { username, password } = this.state;
    if (!username || !password) {
      return this.setState({ error: "Fill all fields!" });
    }
    let loggedIn = this.props.context.login(username, password);
    if (!loggedIn) {
      this.setState({ error: "Invalid Credentails" });
    }
  };
  render() {
    return !this.props.context.user ? (
      <Fragment>
        <div className="hero is-primary ">
          <div className="hero-body container">
            <h4 className="title">Login</h4>
          </div>
        </div>
        <br />
        <br />
        <div className="columns is-mobile is-centered">
          <div className="column is-one-third">
            <div className="field">
              <label className="label">User Name: </label>
              <input
                className="input"
                type="text"
                name="username"
                onChange={this.handleChange}
              />
            </div>
            <div className="field">
              <label className="label">Password: </label>
              <input
                className="input"
                type="password"
                name="password"
                onChange={this.handleChange}
              />
            </div>
            {this.state.error && (
              <div className="has-text-danger">{this.state.error}</div>
            )}
            <div className="field is-clearfix">
              <button
                className="button is-primary is-outlined is-pulled-right"
                onClick={this.login}
              >
                Submit
              </button>
            </div>
          </div>
        </div>
      </Fragment>
    ) : (
      <Redirect to="/products" />
    );
  }
}
export default withContext(Login);

This component renders a form with two inputs to collect the user login credentials. On submit of the input the component calls the login method, which is passed through the context. This Module also makes sure to redirect to the products page if the user is already logged in. We need to update the App.js component to use the Login component:

// ./src/App.js
...
import Login from "./components/Login";
...
...
    <Route exact path="/login" component={Login} />
...

Login

Products and Cart

Next, we create the products page, which will also act as the app landing page. This page will make use of two new components. The first is ProductList.js, which will show the page body, and the other is the ProductItem.js component for each product in the list. Before proceeding to build those components, we need to load the products in the App component. First, we set a default value for products in the constructor an empty array:

//./src/App.js
...  
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      products: []
    };
  }
...

We then need to load the products on the component mount, as we did for the logged-in user:

//.src/App/.js
...
  componentDidMount() {
    let user = localStorage.getItem("user");
    let products = localStorage.getItem("products");

    user = user ? JSON.parse(user) : null;
    products = products ? JSON.parse(products) : data.initProducts;

    this.setState({ user, products });
  }
...

In the code snippet above, we’ve attempted to load products from the local storage in case changes have been made to the initial product list. If it doesn’t exist, we load the default list of products from the app sample data.

Before creating the product listing components, let’s create an interface to create a product for users with the right privileges. First, let’s define the method to add the product. We’ll do that in in the App component, as shown below:

//.src/App/.js
...
  addProduct = (product, callback) => {
    let products = this.state.products.slice();
    products.push(product);
    localStorage.setItem("products", JSON.stringify(products));
    this.setState({ products }, () => callback && callback());
  };
...

This method receives the product object and appends it to the array of products, then saves it to the app state and local storage. It also receives a callback function to execute on successfully adding the product.

Now we can proceed to make the add product component:

//.src/components/AddProduct.js

import React, { Component, Fragment } from "react";
import withContext from "../withContext";
import { Redirect } from "react-router-dom";
const initState = {
  name: "",
  price: "",
  stock: "",
  shortDesc: "",
  description: ""
};
class AddProduct extends Component {
  constructor(props) {
    super(props);
    this.state = initState;
  }
  save = e => {
    e.preventDefault();
    const { name, price, stock, shortDesc, description } = this.state;
    if (name && price) {
      this.props.context.addProduct(
        {
          name,
          price,
          shortDesc,
          description,
          stock: stock || 0
        },
        () => this.setState(initState)
      );
    } else {
      this.setState({ error: "Please Enter name and price" });
    }
  };
  handleChange = e =>
    this.setState({ [e.target.name]: e.target.value, error: "" });
  render() {
    const { name, price, stock, shortDesc, description } = this.state;
    const { user } = this.props.context;
    return !(user && user.accessLevel < 1) ? (
      <Redirect to="/" />
    ) : (
      <Fragment>
        <div className="hero is-primary ">
          <div className="hero-body container">
            <h4 className="title">Login</h4>
          </div>
        </div>
        <br />
        <br />
        <form onSubmit={this.save}>
          <div className="columns is-mobile is-centered">
            <div className="column is-one-third">
              <div className="field">
                <label className="label">Product Name: </label>
                <input
                  className="input"
                  type="text"
                  name="name"
                  value={name}
                  onChange={this.handleChange}
                  required
                />
              </div>
              <div className="field">
                <label className="label">Price: </label>
                <input
                  className="input"
                  type="number"
                  name="price"
                  value={price}
                  onChange={this.handleChange}
                  required
                />
              </div>
              <div className="field">
                <label className="label">Available in Stock: </label>
                <input
                  className="input"
                  type="number"
                  name="stock"
                  value={stock}
                  onChange={this.handleChange}
                />
              </div>
              <div className="field">
                <label className="label">Short Description: </label>
                <input
                  className="input"
                  type="text"
                  name="shortDesc"
                  value={shortDesc}
                  onChange={this.handleChange}
                />
              </div>
              <div className="field">
                <label className="label">Description: </label>
                <textarea
                  className="textarea"
                  type="text"
                  rows="2"
                  style={{ resize: "none" }}
                  name="description"
                  value={description}
                  onChange={this.handleChange}
                />
              </div>
              {this.state.error && (
                <div className="error">{this.state.error}</div>
              )}
              <div className="field is-clearfix">
                <button
                  className="button is-primary is-outlined is-pulled-right"
                  type="submit"
                  onClick={this.save}
                >
                  Submit
                </button>
              </div>
            </div>
          </div>
        </form>
      </Fragment>
    );
  }
}
export default withContext(AddProduct);

This component renders a form to collect the necessary information and also to make sure the user is logged in and has the required access level required to create a product. It has a save method that uses the user’s input to create a product object and calls the addProduct method from the context and passes the product and a callback method to reset the input fields.

Refined login

We can now add this component to the App component for its routing:

// ./src/App.js
...
import Login from "./components/AddProduct";
...
...
    <Route exact path="/add-product" component={AddProduct} />
...

Next, we proceed to create the Productlist component, as shown below:

// ./src/components/ProductList

import React, { Component, Fragment } from "react";
import ProductItem from "./ProductItem";
import withContext from "../withContext";
const ProductList = props => {
  const { products } = props.context;
  return (
    <Fragment>
      <div className="hero is-primary">
        <div className="hero-body container">
          <h4 className="title">Our Products</h4>
        </div>
      </div>
      <br />
      <div className="container">
        <div className="column columns is-multiline">
          {products && products.length ? (
            products.map((product, index) => (
              <ProductItem
                product={product}
                key={index}
                addToCart={props.context.addToCart}
              />
            ))
          ) : (
            <div className="column">
              <span className="title has-text-grey-light">
                No product found!
              </span>
            </div>
          )}
        </div>
      </div>
    </Fragment>
  );
};
export default withContext(ProductList);

Since the list is dependent on the context for data, we wrap it with the withContext function as well. This component renders the products using the ProductItem component, which we’re yet to create. It also passes an addToCart method from the context (which we’re also yet to define) to the ProductItem. This eliminates the need to work with context direct in the ProductItem component. Next, we create the ProductItem component:

./src/components/ProductItem.js

import React, { Component } from "react";
const ProductItem = props => {
  const { product } = props;
  return (
    <div className=" column is-half">
      <div className="box">
        <div className="media">
          <div className="media-left">
            <figure className="image is-64x64">
              <img
                src="https://bulma.io/images/placeholders/128x128.png"
                alt="Image"
              />
            </figure>
          </div>
          <div className="media-content">
            <b style={{ textTransform: "capitalize" }}>
              {product.name}{" "}
              <span className="tag is-primary">${product.price}</span>
            </b>
            <div>{product.shortDesc}</div>
            {product.stock > 0 ? (
              <small>{product.stock + " Available"}</small>
            ) : (
              <small className="has-text-danger">Out Of Stock</small>
            )}
            <div className="is-clearfix">
              <button
                className="button is-small is-outlined is-primary   is-pulled-right"
                onClick={() =>
                  props.addToCart({
                    id: product.name,
                    product,
                    amount: 1
                  })
                }
              >
                Add to Cart
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};
export default ProductItem;

This element displays the product on a can and also provides an action button to add the product to the user’s cart.

Products

Next, we create the cart object and the cart page. First, let’s create the cart in the App component and also make sure that we load existing the cart from the local storage on App component load:

//./src/App.js
...  
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      products: [],
      Cart: {}
    };
  }
...
...
  componentDidMount() {
    let user = localStorage.getItem("user");
    let products = localStorage.getItem("products");
    let cart = localStorage.getItem("cart");


    products = products ? JSON.parse(products) : data.initProducts;
    user = user ? JSON.parse(user) : null;
    cart = cart? JSON.parse(cart) : {};

    this.setState({ user, products, cart });
  }
...

Next, we need to define the cart functions. First, we create the addToCart method:

//./src/App.js
...
  addToCart = cartItem => {
    let cart = this.state.cart;
    if (cart[cartItem.id]) {
      cart[cartItem.id].amount += cartItem.amount;
    } else {
      cart[cartItem.id] = cartItem;
    }
    if (cart[cartItem.id].amount > cart[cartItem.id].product.stock) {
      cart[cartItem.id].amount = cart[cartItem.id].product.stock;
    }
    localStorage.setItem("cart", JSON.stringify(cart));
    this.setState({ cart });
  };
...

This method appends the item using the item ID as key for the cart object. We’re using an object rather than an array for the cart to enable easy data retrieval. This method checks the cart object if an item with that key exists. If it does, increase the amount; otherwise it creates a new entry. The second if statement ensures that the user can’t add more items than the available items. The method then saves the cart to state, which is passed to other parts of the application via the context. And finally, the method saves the updated cart to local storage for persistency.

Next, we define the removeFromCart method to remove a specific product from the user cart and clearCart to remove all products from the user cart:

//./src/App.js
...
  removeFromCart = cartItemId => {
    let cart = this.state.cart;
    delete cart[cartItemId];
    localStorage.setItem("cart", JSON.stringify(cart));
    this.setState({ cart });
  };
  clearCart = () => {
    let cart = {};
    localStorage.removeItem("cart");
    this.setState({ cart });
  };
...

The removeCart method removes a product using the provided product key. It then updates the app state and local storage accordingly. The clearCart method resets the cart to an empty object on the state and removes the cart entry on local storage.

The cart

Now, we proceed to make the cart user interface. Similar to the list of products, we achieve this using two elements: the first, Cart.js, which renders the page layout, and a list of cart items using the second component, CartItem.js:

// ./src/components/Cart.js

import React, { Fragment } from "react";
import withContext from "../withContext";
import CartItem from "./CartItem";
const Cart = props => {
  const { cart } = props.context;
  const cartKeys = Object.keys(cart || {});
  return (
    <Fragment>
      <div className="hero is-primary">
        <div className="hero-body container">
          <h4 className="title">My Cart</h4>
        </div>
      </div>
      <br />
      <div className="container">
        {cartKeys.length ? (
          <div className="column columns is-multiline">
            {cartKeys.map(key => (
              <CartItem
                cartKey={key}
                key={key}
                cartItem={cart[key]}
                removeFromCart={props.context.removeFromCart}
              />
            ))}
            <div className="column is-12 is-clearfix">
              <br />
              <div className="is-pulled-right">
                <button
                  onClick={props.context.clearCart}
                  className="button is-warning "
                >
                  Clear cart
                </button>{" "}
                <button
                  className="button is-success"
                  onClick={props.context.checkout}
                >
                  Checkout
                </button>
              </div>
            </div>
          </div>
        ) : (
          <div className="column">
            <div className="title has-text-grey-light">No item in cart!</div>
          </div>
        )}
      </div>
    </Fragment>
  );
};
export default withContext(Cart);

The Cart component also passes a method from the context to the CartItem. The Cart component loops through an array of the context cart object values and returns a CartItem for each. It also provides a button to clear the user cart. Next is the CartItem component, which is very much like the ProductItem component but for a few subtle changes:

// ./src/components/CartItem.js

import React, { Component } from "react";
const CartItem = props => {
  const { cartItem, cartKey } = props;
  const { product, amount } = cartItem;
  return (
    <div className=" column is-half">
      <div className="box">
        <div className="media">
          <div className="media-left">
            <figure className="image is-64x64">
              <img
                src="https://bulma.io/images/placeholders/128x128.png"
                alt="Image"
              />
            </figure>
          </div>
          <div className="media-content">
            <b style={{ textTransform: "capitalize" }}>
              {product.name}{" "}
              <span className="tag is-primary">${product.price}</span>
            </b>
            <div>{product.shortDesc}</div>
            <small>{`${amount} in cart`}</small>
          </div>
          <div
            className="media-right"
            onClick={() => props.removeFromCart(cartKey)}
          >
            <span className="delete is-large"></span>
          </div>
        </div>
      </div>
    </div>
  );
};
export default CartItem;

This component shows the product info and the number of items selected. It also provides a button to remove the product from the cart. Now we need to import the Cart component in the App component and route it as shown below:

// ./src/App.js
...
import Login from "./components/Cart";
...
...
    <Route exact path="/cart" component={Cart} />
...

Finally, we need to add the checkout method in the App component:

// ./src/Appjs
...
  checkout = () => {
    if (!this.state.user) {
      this.routerRef.current.history.push("/login");
      return;
    }
    const cart = this.state.cart;
    const products = this.state.products.map(p => {
      if (cart[p.name]) {
        p.stock = p.stock - cart[p.name].amount;
      }
      return p;
    });
    this.setState({ products });
    this.clearCart();
  };
...

This method checks to see that a user is logged in before it proceeds. If the user is not logged in, it redirects the user using the router reference we attached to the Router component.

Typically, in a regular ecommerce site, this is where the billing process would take place, but for our application, we’ll just assume the user has paid and therefore remove their purchased items from the list of available items.

With this, we’ve succeeded in completing our basic shopping cart.

Products

Conclusion

In the course of this article, we used React Context to move data and methods across multiple components. Although we could have achieved the same result by passing those data as props, it may become cumbersome to move data over multiple nested elements. This application is by no means a finished product and could be improved upon. For example, more effort could be taken to alert a user when an operation is done or in progress. The app is just to show how easy it is to build an ecommerce application using React.

The code for this application is available on GitHub.

Want to dive into more React? Check out React Design Patterns and Best Practices and plenty of other React resources on SitePoint Premium.

Deven is an entrepreneur, and full-stack developer, constantly learning and experiencing new things. He currently manages CodeSource.io & Dunebook.com and actively working on projects like WrapPixel

New books out now!

Learn how Git works, and how to use it to streamline your workflow!


Google, Netflix and ILM are Python users. Maybe you should too?