Source: https://dzone.com/articles/create-a-simple-shopping-cart-using-react-and-node
by George Anderson
Web development has never been more
exciting than now, with the introduction of new technologies and
techniques, thanks to the dynamism and evolution of JavaScript. Now a
web application can be configured using only JavaScript. This is made
possible through the introduction of Node.js, which can be used on the
server-side to manage data, and also the creation of several JavaScript
frameworks, such as React.js, Angular, and Vue.js, which manage and
control the client-side of the application.
In this article, we will be learning how to build a simple
shopping cart app, using React.js as the front-end framework, and a
backend server built using Node.js and Express.js.
Below is a preview of what we will be building:
Configuring the Development Environment
For this tutorial, I will be using the Eclipse IDE with the CodeMix plugin installed.
- Download the Eclipse IDE here.
- Install CodeMix from the Eclipse marketplace or via genuitec.com.
- Install the React extension using the CodeMix extension browser.
Creating a React Project Using CodeMix
We can now create our application using the CodeMix Project
Wizard. We'll be using the latest version of all the tech libraries and
stacks as at the time of this writing. To create a new React Project
Navigate to File> New > Project > CodeMix > React Project.
Now click Next and enter your project name on the next
screen. Finally, click Finish to continue with the React project
creation.
Let's install some modules which we will need in building the
application. Open the integrated Terminal+ in CodeMix 2.0.
Alternatively, it can be opened using the command
Ctrl + Shift + P
, as shown below, to run the following command:npm i -S express react-route-dom axios jsonwebtoken cors body-parser boostrap
These are the technologies we will be using for our app:
- Cors: it provides a middleware to handle cross-origin resource sharing.
- Axios: it is a Promise-based HTTP client used to communicate with the Node server.
- Express: it is a Node.js module that simplifies the creation of a node server.
- JSON Web Token (JWT): it is a compact and self-contained way for securely transmitting information between parties as a JSON object.
- React Router: it is used in handling the application routing.
- Bootstrap: it is a CSS framework.
Setting Up the Backend Server
In this section, we will be building the Application Programming Interface (API) for the application. First, we create an
api
folder in the root folder of our project. This folder will contain the express backend. Next, we proceed with creating a server.js
file which will contain the server configuration.'use strict';
const express = require('express');
const app = express();
const jwt = require('jsonwebtoken');
const cors = require('cors');
const bodyParser = require('body-parser');
const data = require('./data');
const middleware = require('./middleware');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
app.get('/api/products', (req, res) => { //lists all available products
return res.json(data.products);
});
app.post('/api/products', (req, res) => { //generates the list of products in the cart
let products = [], id = null;
let cart = JSON.parse(req.body.cart);
if (!cart) return res.json(products)
for (var i = 0; i < data.products.length; i++) {
id = data.products[i].id.toString();
if (cart.hasOwnProperty(id)) {
data.products[i].qty = cart[id]
products.push(data.products[i]);
}
}
return res.json(products);
});
app.post('/api/auth', (req,res) => { //signs in user
let user = data.users.filter((user) => {
return user.name == req.body.name && user.password == req.body.password;
});
if (user.length){
// create a token using user name and password vaild for 2 hours
let token_payload = {name: user[0].name, password: user[0].password};
let token = jwt.sign(token_payload, "jwt_secret_password", { expiresIn: '2h' });
let response = { message: 'Token Created, Authentication Successful!',
token: token };
// return the information including token as JSON
return res.status(200).json(response);
} else {
return res.status("401").json("Authentication failed. admin not found.");
}
});
app.get('/api/pay', middleware, (req, res) => { //checkout route for signed in users
return res.json("Payment Successful!");
});
const PORT = 5000;
app.listen(PORT);
console.log('api runnging on port ' + PORT + ': ');
This
server.js
file contains four routes, the first of which returns an array of products loaded from the data.js
module (which also contains an array of valid users).const products = [
{
id: 01,
name: 'Cool Vex',
available_quantity: 5,
price: 450,
description: 'Lorem ipsum dolor sit amet, iusto appellantur vix te, nam affert feugait menandri eu. Magna simul ad est. Nostrum neglegentur ius at, at pertinax repudiare vel. Vim an adolescens quaerendum.'
},
{
The second route is a post route that accepts a cart string,
parses it, and generates the list products on the cart, along with
their quantities.
The third is the login route that authenticates the user,
i.e. verifies the user is valid (registered). It generates a JSON web
token that expires in two hours. The generated token is used to bypass
the login middleware (which we will create next).
The fourth route is a get route guarded by a middleware.
This route requires the request to be authenticated, i.e. it provides a
valid token. This middleware checks that the token was generated using a
valid user. It also checks to confirm that the token is not expired. To
create the middleware, we make a
middleware.js
file in the api
folder, as shown below:const jwt = require('jsonwebtoken');
const JWT_SECRET = "jwt_secret_password";
module.exports = (req, res, next) => {
// check header or url parameters or post parameters for token
var token = req.body['x-access-token'] || req.query['x-access-token'] || req.headers['x-access-token'];
// decode token
if (token) {
// verifies secret and checks exp
jwt.verify(token, JWT_SECRET, function(err, decoded) {
if (err) {
return res.status(403).send({
success: false,
message: 'Failed to authenticate token.'
});
} else {
// if everything is good, save to request for use in other routes
req.decoded = decoded;
next();
}
});
} else {
// if there is no token, return an error
return res.status(401).send({
success: false,
message: 'No token provided.'
});
}
};
If a request is made to a guarded route without a valid
token, the middleware intercepts the request and returns a 401
(Unauthorized) error.
Next, we include a script in the scripts object in the
package.json
file, which we will use to start up the application API:{
"name": "shopping-cart",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.18.0",
"body-parser": "^1.18.3",
"cors": "^2.8.5",
"express": "^4.16.4",
"jsonwebtoken": "^8.4.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-router-dom": "^4.3.1",
"react-scripts": "2.1.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"api": "node api/server.js"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
With that, we have completed the development of the API
server, which we can then run using the command below in the integrated
terminal:
npm run api
Setting Up the Front-End
Now that we have the application's backend running, let's begin developing its front-end.
From the root of the project, we will open a new terminal using the
Ctrl + Shift + P
command. In the terminal, we will run the following command to start the React application:npm start
This application will be styled using Bootstrap. This can be
achieved in several ways, one of which is to install the React
bootstrap module, as we had previously done. We then import it into the
root React component, i.e. the
index.js
, as shown below:import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render(<App />, document.getElementById('root'));
serviceWorker.unregister();
Let’s create a
repository.js
file which will contain all the interactions with the backend server, using the axios module.import axios from 'axios';
const BASE_URL = 'http://localhost:5000';
export function getProducts() {
return axios.get(`${BASE_URL}/api/products`)
.then(response => response.data);
}
export function getCartProducts(cart) {
return axios.post(`${BASE_URL}/api/products`, {cart})
.then(response => response.data);
}
export function login (data) {
return axios.post(`${BASE_URL}/api/auth`,
{ name: data.name, password: data.password })
.then(response => {
localStorage.setItem('x-access-token', response.data.token);
localStorage.setItem('x-access-token-expiration',
Date.now() + 2 * 60 * 60 * 1000);
return response.data})
.catch(err => Promise.reject('Authentication Failed!'));
}
export function pay (data) {
return axios.get(`${BASE_URL}/api/pay`,
{ params: { 'x-access-token': localStorage.getItem('x-access-token')} })
.then(response => response.data)
.catch(err => Promise.reject(err));
}
export function isAuthenticated(){
return localStorage.getItem('x-access-token') && localStorage.getItem('x-access-token-expiration') > Date.now()
}
The
getProducts
method fetches all the available products from the server. The getCartProducts
fetches all the products in the cart by passing the cart string to the
server. This cart string is parsed to an object, with its keys as the
product ID, and the values as the requested quantities. The login method
submits the user credentials to the backend server. It also stores the
generated token along with the token's expiration time in the local
storage of the browser upon a successful request. The pay method is only
available for authenticated users because it requires a valid access
token. The final method is used to verify that the user is authenticated
by checking to see if there is a valid token that is not expired.Creating Components
Let's proceed with creating a
src/components
folder which will contain the application's components.
In the application, we will be creating six other components which include
Login
, ProductItem
, ProductList
, CartItem
, Cart
, and Checkout
, in addition to the root App
component.
The JSX editing capabilities in CodeMix make it easy to work
with React code. Just take a look at the GIFs below to see how you can
take advantage of the editing capabilities in CodeMix, as you work your
way through these components.
First, let's create the
Login
component. import React from 'react';
import { login } from '../repository';
export default class Login extends React.Component{
constructor() {
super();
this.state = { name: '', password: '' };
}
handleInputChange = (event) =>
this.setState({[event.target.name]: event.target.value})
submitLogin = (event) => {
event.preventDefault();
login(this.state)
.then(token => window.location = '/').catch(err => console.log(err));
}
render() {
return (
<div className="container">
<hr/>
<div className="col-sm-8 col-sm-offset-2">
<div className="panel panel-primary">
<div className="panel-heading"><h3>Log in </h3></div>
<div className="panel-body">
<form onSubmit={this.submitLogin}>
<div className="form-group">
<label>Name:</label>
<input type="text" className="form-control"
name="name" onChange={this.handleInputChange}/>
</div>
<div className="form-group">
<label>Password:</label>
<input type="password" className="form-control"
name="password" onChange={this.handleInputChange}/>
</div>
<button type="submit" className="btn btn-success">Submit</button>
</form>
</div>
</div>
</div>
</div>
);
}
}
This component gets the username and password, passes it on
to the backend server when the form is submitted, using the login method
from the repository module, and redirects the user to the home URL upon
a successful request.
Let's create the
ProductItem
component, which we will be used to render each product on the product list. import React from 'react';
export default class ProductItem extends React.Component {
constructor(props) {
super(props);
this.state = {quantity: 1}
}
handleInputChange = event =>
this.setState({[event.target.name]: event.target.value})
addToCart = () => {
let cart = localStorage.getItem('cart')
? JSON.parse(localStorage.getItem('cart')) : {};
let id = this.props.product.id.toString();
cart[id] = (cart[id] ? cart[id]: 0);
let qty = cart[id] + parseInt(this.state.quantity);
if (this.props.product.available_quantity < qty) {
cart[id] = this.props.product.available_quantity;
} else {
cart[id] = qty
}
localStorage.setItem('cart', JSON.stringify(cart));
}
render(){
const { product } = this.props;
return (
<div className="card" style={{ marginBottom: "10px"}}>
<div className="card-body">
<h4 className="card-title">{product.name}</h4>
<p className="card-text">{product.description}</p>
<h5 className="card-text"><small>price: </small>${product.price}</h5>
<span className="card-text">
<small>Available Quantity: </small>{product.available_quantity}
</span>
{ product.available_quantity > 0 ?
<div>
<button className="btn btn-sm btn-warning float-right"
onClick={this.addToCart}>Add to cart</button>
<input type="number" value={this.state.quantity} name="quantity"
onChange={this.handleInputChange} className="float-right"
style={{ width: "60px", marginRight: "10px", borderRadius: "3px"}}/>
</div> :
<p className="text-danger"> product is out of stock </p>
}
</div>
</div>
)
}
}
The
addToCart
method adds the given product to the cart which is an object stored in localStorage
as a string using the JSON.stringify
method. This method converts the string back to object using the JSON.parse
method or creates a new object if no item is found. The product is then added, and the cart is saved back in localStorage
.
Let's create the Product List which uses the
ProductItem
component to render the list of products. import React from 'react';
import ProductItem from './ProductItem';
import { getProducts } from '../repository';
import { Link } from 'react-router-dom';
export default class ProductList extends React.Component {
constructor(props) {
super(props);
this.state = {
products: []
}
}
componentDidMount() {
getProducts().then((products) =>this.setState({ products }));
}
render() {
const { products } = this.state;
return (
<div className=" container">
<h3 className="card-title">List of Available Products</h3><hr/>
{products.map((product, index) => <ProductItem product={product} key={index}/>)}
<hr/>
<Link to="/checkout">
<button className="btn btn-success float-right">Checkout</button>
</Link>
<Link to="/cart">
<button className="btn btn-primary float-right"
style={{ marginRight: "10px" }}>View Cart</button>
</Link><br/><br/><br/>
</div>
);
}
}
This component fetches the array of available products using the
getProducts
from the repository module in the componentDidMount
lifecycle provided by React.
Let's create the
CartItem
component which will be used to render each product on the cart. import React from 'react';
export default class CartItem extends React.Component {
constructor(props) {
super(props);
this.state = {quantity: 1}
}
render(){
const { product } = this.props;
return (
<div className="card" style={{ marginBottom: "10px"}}>
<div className="card-body">
<h4 className="card-title">{product.name}</h4>
<h5 className="card-text"><small>price: </small>${product.price}</h5>
<span className="card-text text-success">
<small>Quantity: </small>{product.qty}</span>
<button className="btn btn-sm btn-warning float-right"
onClick={() => this.props.remove(product)}>Remove from cart</button>
</div>
</div>
)
}
}
This component uses the
remove
method provided as a prop to remove the item from the cart completely. The remove
method is provided by the parent Cart
component which we will be creating next. import React from 'react';
import { Link } from 'react-router-dom';
import { getCartProducts } from '../repository';
import CartItem from './CartItem';
export default class Cart extends React.Component {
constructor(props) {
super(props);
this.state = { products: [], total: 0 }
}
componentDidMount() {
let cart = localStorage.getItem('cart');
if (!cart) return;
getCartProducts(cart).then((products) => {
let total = 0;
for (var i = 0; i < products.length; i++) {
total += products[i].price * products[i].qty;
}
this.setState({ products, total });
});
}
removeFromCart = (product) => {
let products = this.state.products.filter((item) => item.id !== product.id);
let cart = JSON.parse(localStorage.getItem('cart'));
delete cart[product.id.toString()];
localStorage.setItem('cart', JSON.stringify(cart));
let total = this.state.total - (product.qty * product.price)
this.setState({products, total});
}
clearCart = () => {
localStorage.removeItem('cart');
this.setState({products: []});
}
render() {
const { products, total } = this.state;
return (
<div className=" container">
<h3 className="card-title">Cart</h3><hr
{
products.map((product, index) =>
<CartItem product={product} remove={this.removeFromCart} key={index}/>)
} <hr/>
{ products.length ?
<div><h4>
<small>Total Amount: </small>
<span className="float-right text-primary">${total}</span>
</h4><hr/></div>: ''}
{ !products.length ?<h3 className="text-warning">No item on the cart</h3>: ''}
<Link to="/checkout">
<button className="btn btn-success float-right">Checkout</button></Link>
<button className="btn btn-danger float-right" onClick={this.clearCart}
style={{ marginRight: "10px" }}>Clear Cart</button><br/><br/><br/>
</div>
);
}
}
The
removeFromCart
method in this component is passed to the CartItem
component. It deletes the product from the cart on the localStorage
and removes the product from the list products to be rendered. The Cart
component also provides a clearCart
method which removes all the items on the cart (this is done by deleting the cart from the localStorage
).
Let's create the
Checkout
component. This component will only be rendered if the user authenticated (logged in). import React from 'react';
import { isAuthenticated, getCartProducts, pay } from '../repository';
import { Redirect, Link } from 'react-router-dom';
export default class Checkout extends React.Component {
constructor(props) {
super(props);
this.state = { products: [], total: 0 }
}
componentDidMount() {
let cart = localStorage.getItem('cart');
if (!cart) return;
getCartProducts(cart).then((products) => {
let total = 0;
for (var i = 0; i < products.length; i++) {
total += products[i].price * products[i].qty;
}
this.setState({ products, total });
});
}
pay = () => pay().then(data => alert(data)).catch(err => console.log(err))
render() {
if (!isAuthenticated()) return (<Redirect to="/login" />);
const { products, total } = this.state;
return (
<div className=" container">
<h3 className="card-title">Checkout</h3><hr/>
{ products.map((product, index) =>
<div key={index}>
<p>{product.name} <small> (quantity: {product.qty})</small>
<span className="float-right text-primary">${product.qty * product.price}
</span></p><hr/>
</div>
)} <hr/>
{ products.length ?
<div><h4><small>Total Amount:</small><span className="float-right text-primary">
${total}</span></h4><hr/></div>: ''}
{ !products.length ? <h3 className="text-warning">No item on the cart</h3>: ''}
{ products.length ? <button className="btn btn-success float-right"
onClick={this.pay}>Pay</button>: '' }
<Link to="/"><button className="btn btn-danger float-right"
style={{ marginRight: "10px" }}>Cancel</button></Link><br/><br/><br/>
</div>
);
}
}
This component loads and renders all the products on the cart, much like the
Cart
component. It also provides a pay button which will in this case alert a
message to proceed to payment after accessing a secured route on the
backend server. This is made possible by the access token attached to
the request in the pay method in the repository module. It also
redirects unauthenticated users to the login page.
Now that we have all the required components, we can proceed with editing the root
App
component: import React, { Component } from 'react';
import Login from './components/Login';
import Products from './components/ProductList';
import Cart from './components/Cart';
import Checkout from './components/Checkout';
import { BrowserRouter as Router, Link, Route } from 'react-router-dom';
import { isAuthenticated } from './repository';
class App extends Component {
logOut(){
localStorage.removeItem('x-access-token');
}
render() {
const auth = isAuthenticated();
return (
<Router>
<div>
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div className="container">
<Link className="navbar-brand" to="/">ShoppingCart</Link>
<button className="navbar-toggler" type="button"
data-toggle="collapse" data-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup" aria-expanded="false"
aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNavAltMarkup">
<div className="navbar-nav">
<Link className="nav-item nav-link" to="/">Products</Link>
<Link className="nav-item nav-link" to="/cart">Cart</Link>
{ (auth) ? <Link className="nav-item nav-link" to="/checkout">
Checkout</Link>: ''}
{ ( auth ) ?
( <a className="nav-item nav-link" href="/"
onClick={this.logOut}>Log out</a>) :
( <Link className="nav-item nav-link float-right"
to="/login">Log in</Link> )
}
</div>
</div>
</div>
</nav>
<div className="container">
<br/>
<Route exact path="/" component={Products} />
<Route exact path="/cart" component={Cart} />
<Route exact path="/checkout" component={Checkout} />
{ (!auth) ? <Route exact path="/login" component={Login} /> : '' }
</div>
</div>
</Router>
);
}
}
export default App;
In this component, we create, control, and manage the
application's navigation. Here, we render the navigation bar which in
turn renders several links using the
Link
tag provided by react-router-dom
, which is roughly equivalent to the HTML "a" tag. It renders the Login
link when the user is not logged in, and renders Checkout
and Log out
links if otherwise. The log out button triggers the logOut
method which deletes the access token and renders the application with
the user logged out. This component also manages the application
routing, i.e. it chooses which component to render for each route or
link. To enable the use of react-router-dom
components in the application, we need to wrap the app component in a BrowserRouter
component.
Congratulations! We have just successfully built the entire application!
To recap the application start-up process, to run the
application, we need to run both the backend and front-end of the
application. To start the backend API server of the application, we use
the command:
npm run api
To start the front end, use the command below:
npm start
After those have been successfully started, we can visit http://localhost:3000 in our browser to view the application.
In Closing
Over the course of this article, we've built a simple
shopping cart with an Express backend and a React front-end. We've kept
this application simple to demonstrate core Express and React concepts.
Next steps would be to change the backend to interact with a database
instead of static data, and use technologies, such as Flux, Redux, Mobx,
etc., to make the front-end more efficient and modular.
The code for this application is available here.