articles

How to Create an E-Commerce Site with React?

Today we will learn how to build a simple e-commerce store in React. Hopefully, it will help you understand React better and see how it is best suited to build dynamic UI/UX.

 

This ecommerce store app will have simple cart management and user authentication. We will also be using the JSON server to create a pseudo back end.

What You Will Need 

 

To create this store you must first be equipped with
 
    ● Basic knowledge of React and working with JavaScript.
 
    ● The latest version of Node with the correct binaries.
 

Setting Up The Tools 

 
Start by installing the node as following:
 
node -v
>12.18.4

npm -v
>6.14.8
 
Then launch the create react app tool and start a new React project as follows:
 
create-react-app e-commerce store
 
After creating the new project change the directory to the project’s directory.
 
cd e-commerce store
 
Since we will be handling routing, we will use the React router tool to handle the tool for us. Install the module by using the following command:
 
npm install react-router-dom
 
For handling the authentication, we will need a server and authentication protocols for our back end. So we will create a fake back end with json-server and json-server-auth as follows:
 
npm install json-server json-server-auth
 
To handle the Ajax requests we will use Axios so install Axios with the following command:
 
npm install axios
 
For parsing the JWT, we are going to need the jet-decode:
 
npm install jwt-decode
 
For this project, we will use the Bulma CSS to create the front end style. Install as follows:
 
npm install bulma

Getting Started

 

To start, we will first add a common stylesheet to be used across all components. We will import bulma.css and include this in the src folder as well as the index.js.
 
import "bulma/css/bulma.css";
 

Creating Context

 

To create the context, we will need to create a context.js file and a withContext.js file in the source directory of the application.
 
cd src

touch Context.js withContext.js
 
After this, use the following code to create the context and initialize the context data:
 
import React from "react";
 
const Context = React.createContext({}); 
 
export default Context;
 
Then wrap the components with a component wrapper like this:
 
// src/withContext.js

import React from "react";
import Context from "./Context";
const with Context = WrappedComponent=>{
const With HOC= props=>{
return(
<Context.Consumer>
{context => <WrappedComponent{...props}context = {context}/>}
</Context.Consumer>
);
};
return WithHOC;
};
exportdefaultwithContext;
 
All we have done here is that we have created a higher-order component that has appended our context to the component props that we just wrapped.
 

Scaffolding

 
Now we will need to create the basic structure of the components that will assist in the basic navigation of our app and provide basic functionality. These components will be our cart, login tool, product listings, and the additional products feature. We will place these components in the components directory inside the source (src) directory.
 
mk dir components
cd components
touch AddProduct.js Cart.js Login.js ProductList.js
 
Add the following code one by one to each component:

For addproduct.js

 
import React from "react";<
export default function AddProduct() {
return <>AddProduct</>
}

For cart.js 

 
import React from "react";

export default function Cart() {
return <>Cart</>
}

For Login.js

 
import React from "react";

export default function Login() {
return <>Login</>
}

For ProductList

 
import React from "react";

export default function ProductList() {
return <>ProductList</>
}
 

Navigation

 

Now that we are done with our basic components we will set up another component app.js. This will be used to handle the app’s navigation and to define its data handling methods.
 
First we will set up the navigation. Add the following to app.js:
 
import React, { Component} from "react";

import{ Switch, Route, Link, Browser Routeras Router } from "react-router-dom";
import AddProduct from './components/AddProduct';
import Cart from './components/Cart';
import Login from './components/Login';
import ProductList from './components/ProductList';
import Context from "./Context";
export default class AppextendsComponent{
constructor (props) {
super (props);
this.state = {
user: null,
cart: {},
products: []
};
this.router Ref=React.createRef();
}
render(){
return(
<Context.Provider
value = {{
...this.state,
remove FromCart:this.remove FromCart,
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>
<label
role = "button"
class = "navbar-burger burger"
aria-label = "menu"
aria-expanded = "false"
data-target = "navbar Basic Example"
onClick={e =>{
e.prevent Default();
this.setState({showMenu:!this.state.showMenu});
}}
>
<span aria-hidden = "true"></span>
<span aria-hidden = "true"></span>
<span aria-hidden = "true"></span>
</label>
</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>
):(
<Link to = "/" onClick = { this.logout } className = "navbar-item">
Logout
</Link>
)}
</div>
</nav>
<Switch>
<Route exactpath = "/" component = { ProductList }/>
<Route exact path = "/login" component = { Login }/>
<Route exact path = "/cart" component = { Cart }/>
<Route exact path = "/add-product" component = { AddProduct }/>
<Route exact path = "/products" component = { ProductList }/>
</Switch>
</div>
</Router>
</Context.Provider>
);
}
}
 
This component will initialize app data and will define methods of manipulating the same. We have first defined the context data and its methods using the context.provider component. Then we have built the navigation by wrapping the app with the router component. The component can be both browser or hash type router.
 
The next step involves defining the routes of our application by using the Route and Switch components. We have also created the navigation menu using the link component from React’s Router module. Then we have added the routerRef as a reference to the Router component so that we can access the router from the app component.
 
To test if everything is working, navigate to the project root directory and start a dev server using npm start. Once booted, you should see your default browser’s window opening and should see the application with a basic skeleton structure. Test if everything in the navigation works properly.
 

Setting The Back End

 

This step involves creating a pseudo back end that will store the products and will handle the user authentication requests. For this purpose, we will use the JSON-server as mentioned before to create the fake API and the JSON-server-auth to add the authentication mechanism to the app.
 
We will create a new folder titled backend and in the folder and a database file called db.json.
 
mkdir backend

cd backend
touchdb.json
In the database file add the following code
{
"users":[
{
"email":"email@mail.com",
"password":"$2a$10$2myKMolZJoH.q.cyXClQXufY1Mc7ETKdSaQQCC6Fgtbe0DCXRBELG",
"id":1
},
{
"email":"admin@admin.com",
"password":"$2a$10$w8qB40MdYkMs3dgGGf0Pu.xxVOOzWdZ5/Nrkleo3Gqc88PF/OQhOG",
"id":2
}
],
"products":[
{
"id":"hdmdu0t80yjkfqselfc",
"name":"shoes",
"stock":10,
"price":399.99,
"shortDesc":"Nullafacilisi. Curabitur at lacus ac velitornarelobortis.",
"description":"Crassagittis. Praesentnecnisl a purusblanditviverra. Utleo. Donec quam felis, ultriciesnec, pellentesqueeu, pretiumquis, sem. Fusce a quam."
},
{
"id":"3dc7fiyzlfmkfqseqam",
"name":"bags",
"stock":20,
"price":299.99,
"shortDesc":"Nullafacilisi. Curabitur at lacus ac velitornarelobortis.",
"description":"Crassagittis. Praesentnecnisl a purusblanditviverra. Utleo. Donec quam felis, ultriciesnec, pellentesqueeu, pretiumquis, sem. Fusce a quam."
},
{
"id":"aoe8wvdxvrkfqsew67",
"name":"shirts",
"stock":15,
"price":149.99,
"shortDesc":"Nullafacilisi. Curabitur at lacus ac velitornarelobortis.",
"description":"Crassagittis. Praesentnecnisl a purusblanditviverra. Utleo. Donec quam felis, ultriciesnec, pellentesqueeu, pretiumquis, sem. Fusce a quam."
},
{
"id":"bmfrurdkswtkfqsf15j",
"name":"shorts",
"stock":5,
"price":109.99,
"shortDesc":"Nullafacilisi. Curabitur at lacus ac velitornarelobortis.",
"description":"Crassagittis. Praesentnecnisl a purusblanditviverra. Utleo. Donec quam felis, ultriciesnec, pellentesqueeu, pretiumquis, sem. Fusce a quam."
}
]
}
 
Here we have created two resources — the ‘users’ resource and the ‘products’. If you see the code, you will see that each user will have a predefined email and an encrypted password.
 
The next step is to start the server. Execute the following command from the root of the project:
 
./node_modules/.bin/json-server-auth ./backend/db.json --port 3001
 
Since we have used json-server-auth, the middleware allows us to simulate the login procedure by giving a login endpoint. You can test it using hopscotch for posting and logging in methods. You should receive a web token that would be valid for one hour.
 
The JSON token should be saved in the client and then sent to the server whenever data is requested. To prevent any reverse engineering, you should repeat this process for every protected resource on your database.
 

Authentication in React

 

For this, we will need Axios and jwt decode and Axios packages. Import these from the app.js.
 
import axios from 'axios';

import jwt_decode from 'jwt-decode';
 
We want to make sure that once the app starts, the user should be already loaded. For this, we will set the user on componentDidMount and add this to the app component. This will allow the app to load the last user session to the current state.
 
componentDidMount( ){

let user = localStorage.getItem("user");
user = user ? JSON.parse(user): null;
this.setState({ user });
}
 

Login and Logout Methods

 

The Next step will be defining the login and logout methods.
 
login = async(email, password) => {

const res = await axios.post (
'http://localhost:3001/login',
{ email, password },
).catch((res) => {
return { status: 401, message: 'Unauthorized' }
})
if (res.status === 200) {
const{ email } = jwt_decode(res.data.accessToken)
const user = {
email,
token:res.data.accessToken,
accessLevel: email === 'admin@example.com'?0:1
}
this.setState({ user });
localStorage.setItem("user", JSON.stringify(user));
return true;
} else {
return false;
}
}
logout = e => {
e.preventDefault();
this.setState({ user: null });
localStorage.removeItem("user");
};
 
This login method will take in all the information in the login form and make an Ajax request to the login endpoint. We will create the login form in a moment.
 
If the returned response is 200, the credentials are deemed to be correct and the token will be decoded to send the user’s email before saving it in the state. If all goes well, the method will return as true, if not then false. This value can be stored in the login components, and we can choose what to display depending on the case.
 
The logout method is a simple one and clears the user from local storage as well as the state.
 

Developing the Login Component

 

Then we must develop the login component. This component will use the context data and for that, it needs to have access to the data methods. We will wrap it in the withContext method. First, change the Login.js as following:
 
import React, { Component } from "react";

import { Redirect } from "react-router-dom";
import with Context from "../withContext";
class Login extends Component{
constructor(props){
super(props);
this.state={
username:"",
password:""
};
}
handleChange = e => this.setState({[e.target.name]:e.target.value, error:""});
login = (e) =>{
e.prevent Default();
const{ username, password } = this.state;
if(!username||!password){
return this.setState({ error: "Fill all fields!" });
}
this.props.context.login(username, password)
.then((loggedIn) =>{
if(!loggedIn){
this.setState({ error: "Invalid Credentails" });
}
})
};
render(){
return!this.props.context.user?(
<>
<div className = "hero is-primary ">
<div className = "hero-body container">
<h4 className = "title">Login</h4>
</div>
</div>
<br/>
<br/>
<form onSubmit = { this.login }>
<div className = "columns is-mobile is-centered">
<div className = "column is-one-third">
<div className = "field">
<label className = "label">Email: </label>
<input
className = "input"
type = "email"
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"
>
Submit
</button>
</div>
</div>
</div>
</form>
</>
):(
<Redirectto = "/products"/>
);
}
}
exportdefaultwithContext(Login);
 
This component will create a form with two input fields to collect the user’s email and password. When the user submits the form, the component triggers the login method which then passes through the context. If the user is already logged in, the page will be redirected to the products page.
 

Displaying Products 

 
Now that we have created the navigation, back end and the data handling methods, we would want to create a product display. We will fetch products from our back end to our front display. This can be done with the component mount in the app component the same way we did for the logged-in user.
 
async componentDidMount(){
    
let user = localStorage.getItem("user");
const products = await axios.get('http://localhost:3001/products');
user = user ? JSON.parse(user) : null;
this.setState({ user, products:products.data });
}
 
Mounting the componentDidMount as async ensures that we can make requests to the products' endpoint.
 

Creating Product Page 

 
Next, we will create a landing page which will be our product’s page. This page will use the ProdcutList.js that we previously created and ProductItem.js we are about to create.
 
Change the Productlist as follows.
 
import React from "react";

import ProductItem from "./ProductItem";
import with Context from "../withContext";
constProductList = props =>{
const{ products } = props.context;
return(
<>
<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 products found!
</span>
</div>
)}
</div>
</div>
</>
);
};
exportdefaultwithContext(ProductList);
 
Since this list is also depending on the context to acquire data, we need to wrap it in the withContext function. This will render the products by the ProductItem component which we will create shortly.
 
This component will also have the addToCart method which will be passed on to the ProductItem.
 
First, create the ProductItem:
 
cdsrc/components

touch ProductItem.js
Then add the following code:
import React from "react";
constProductItem = props =>{
const{ product } = props;
return(
<div className = " column is-half">
<div className = "box">
<div classNam e= "media">
<div className = "media-left">
<figure classNam e= "image is-64x64">
<img
src= "https://bulma.io/images/placeholders/128x128.png"
alt = { product.shortDesc }
/>
</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>
);
};
exportdefaultProductItem;
This element displays a product and provides a button to add the product to the cart.
 

Adding Products

 
Now that we have got a resource of products, we will create an admin interface to add new products.
 
First thing to do is to define the method in the App component to add products as follows:
 
addProduct = (product, callback) => {

let products = this.state.products.slice();
products.push(product);
this.setState({ products },() => callback && callback());
};
 
This method will also receive a callback in case the user successfully adds a product.
 
Now we will structure the AddProduct component.
 
Implement the following code in the AddProduct component:
 
import React,{ Component } from "react";

import with Context from "../withContext";
import{ Redirect } from "react-router-dom";
import axios from 'axios';
constinitState = {
name:"",
price:"",
stock:"",
shortDesc:"",
description:""
};
class AddProduct extends Component{
constructor(props){
super(props);
this.state = initState;
}
save = async(e) =>{
e.preventDefault();
const{ name, price, stock,shortDesc, description } = this.state;
if(name && price){
const id = Math.random().toString(36).substring(2)+Date.now().toString(36);
awaitaxios.post(
'http://localhost:3001/products',
{ id, name, price, stock,shortDesc, description },
)
this.props.context.addProduct(
{
name,
price,
shortDesc, description, stock: stock ||0 }, ()=>this.setState(initState) ); this.setState( { flash:{ status:'is-success',msg:'Product created successfully' }} ); }else{ this.setState( { flash:{ status:'is-danger',msg:'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)?( <Redirectto = "/"/> ):( <> <div className = "hero is-primary "> <div className = "hero-body container"> <h4 className = "title">Add Product</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.flash&&( <div className = {`notification ${this.state.flash.status}`}> { this.state.flash.msg } </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> </> ); } } export defaultwithContext(AddProduct);
 
This is a very complex component and is responsible for several tasks. First, it checks if the user that is currently stored in the context has access and if so, what the access level of the user is. It checks for an access level less than 1. This is applicable in the case if the user is an admin. If it returns a value less than 1, the app will render a form that will allow adding in a new product. If it doesn’t return then value, it will simply return to the product page.
 
This is just an example of course as this can easily be bypassed in the real world. You would want to keep an additional server-side check to ensure that the user is actually permitted to add products as an admin.
 
Once the form is rendered, there will be several fields to fill and some of them will be compulsory like the price and name field. When the user enters any data and submits it, the data is tracked in the state of the component. The save method is then called which makes an Ajax request to the backend we created to create the new product. This will also create a unique UUID for the JSON-server.
 
Then we call the addProduct method to add the created products to the global state.
 

Structuring Cart Management

 

Now we are reaching the final stages of our store and the only thing left to do is to create a cart management interface and method. The cart is already executed in the app.js, but we also want to load the cart from the local storage, so we will update the componentDidMount as follows.
 
asyn ccomponentDidMount(){

let user = localStorage.getItem("user");
let cart = localStorage.getItem("cart");
const products = await axios.get('http://localhost:3001/products');
  user = user ? JSON.parse(user) : null;
  cart = cart ? JSON.parse(cart) : {};
this.setState({ user, products: products.data, cart });
}
 
Then we will create cart functions and addToCart method. First let us create the addToCart method.
 
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 will call the item using the item ID to add to the cart. We will use single objects and not arrays for this purpose so that data retrieval is easy. This method will check for the object in the cart to cross-check if the item already exists. If the item with that key exists, the object count will increase otherwise, it will create a new object entry.
 
The ‘IF’ clause puts a check on the users to prevent them from adding more items than are available. The cart is then saved to the state and is passed on to other relevant parts of the application via our context. Finally, the cart data is stored on the local storage for future use.
 
Next step is to define a method to remove items from the cart. This will be our removeFromCart method. Add as follows:
 
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 });
};
 
This method will remove the product using the product key and updates the app state on the local storage as required. The clearCart method will set the cart to an empty object in state and delete the cart entry from the local storage.
 
Now that we have created the methods, we will proceed to make the user interface. It will be almost similar to the way we created the product lists. We will create two elements — Cart.js which will render the layout of the page and CartItem.js which will render the list of cart items.
 
// ./src/components/Cart.js

import React from "react";
import with Context from "../withContext";
import CartItem from "./CartItem";
constCart= props =>{
const{ cart } = props.context;
const cart Keys = Object.keys(cart ||{});
return(
<>
<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]}
remove FromCart = { 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>
</>
);
};
exportdefaultwithContext(Cart);
 
Next, we will work on the CartItem component. This works similar to the ProductItem component, but there are few changes.
 
Create the component as follows:
 
cd src/components

touch CartItem.js
Add the following code to it:
import React 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= { product.shortDesc }
/>
</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>
);
};
exportdefaultCartItem;
 
This component is responsible for showing the product info and the total number of selected items. It also provides a button to remove any product added to the cart.
 
Now we need to provide a checkout method in our app. We will create the checkout method in the App component as follows:
 
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;
axios.put(
`http://localhost:3001/products/${p.id}`,
{...p },
)
}
return p;
});
this.setState({ products });
this.clearCart();
};
 
During the checkout, this method will allow the application to check if the user is logged in or not before proceeding to the checkout. In case the user hasn’t logged in, the app will direct the user to the login page using routing from the router component.
 
In the real world, this stage would involve payment processing. Since this is a tutorial, we will assume that the transaction has already occurred and the user has paid for the products. After the transaction is completed, Axios will update the stock of your items once an item has been purchased.

Conclusion

 

In this detailed tutorial, we have learnt to use React to create the basic interface of our app and use context to manage data flow in and out of our application through various components and servers.
 
We also created an authentication flow by implementing various checks by using different methods. Although this app is not a final product, it gives you an insight into how it is to build an e-commerce app in React.
 
Let's make your vision a reality. Contact us for a consultation.
Facebook Linkedin