How to Create an Ecommerce Site with React
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:
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.
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} />
...
Continue reading
How to Create an Ecommerce Site with React
on SitePoint.