Recently, I decided on spending more time reading tutorials on the web and making fun projects along the way. One of the projects that caught my eyes is an e-commerce project by Deven Rathore on sitepoint.
I will be using some part of his code and architecture but with some heavy changes and refactoring to its react code likewise. This is a refactoring post.
Deven did his best to the blog posts and code, howbeit, this post aims to better write and rewrite most of its JavaScript and react side and thus build on his existing code and architecture.
Prerequisites
You must be familiar with JavaScript and React. In a nutshell, more than a beginner. This is because I'll be writing some dry code and skip doing so many beginner explanations.
If you are new to JavaScript, you can watch Beau Carnes' 7 hours YouTube video for complete beginners, or if you are the reading type, visit MDN's to get started.Have node installed on your PC. But if you are like me, install nvm-windows because it works better on windows.
Moreover, It is noteworthy to mention you can skip the second prerequisite if the coding is done on codesanbox.
The codebase to this project is accessible here likewise.
Step I
If you are using codesanbox, skip this step
- Dive into your terminal and create a fresh shinny react app.
$: npx create-react-app e-commerce
- Move into the new folder created as a result of successfully executing instruction 1 above.
$: cd e-commerce
Step II
Install the required packages to build our e-commerce app.
$: npm install react-router-dom json-server json-server-auth axios jwt-decode bulma
Interjection β³:
I really never heard of BulmaCSS until I read this post. And O MY GOD, this CSS Framework is super simple to use and cool.
react-router-dom
will be used to handle everything routing and navigation on the app.json-server
does magic for us because it doesn't only provide us with some Json resources to consume, but it also creates a rest API with nothing but a JSON file. You can learn more about the awesomeness of json-server here.json-server-auth
: since we are usingjson-server
for our rest API, provides us with an out-of-the-box authentication flow for json-server. Their documentation is awesome.jwt-decode
will parse JWT that our back end (in this case being json-server) will respond with upon successful login.
And those are the packages that will be used for this project with nothing more π.
Step III
- Remove all the former CSS files in the index.js file like app.css and index.css, and replace them with bulma's.
//../src/index.js ... import ReactDOM from 'react-dom/client'; import 'bulma/css/bulma.css'; // π import App from './App'; ...
Step IV: React Context
At this stage, we would set up React context since I prefer it over redux for smaller projects.
Before we get into the code, permit me to explain react context with an analogy that has helped me master this amazing concept.
React context is the facility provided by the react framework that allows one to skip the redundancy of moving data (a better terminology is props and not data) around several components before getting them to our destination place, usually a consuming component.
state A -> state B -> state C
For example, given that we have a country with three states, A, B, & C above, the direction to reach each of the states is linear. How can one move a parcel from
state A
tostate C
?To get to
state C
with a parcel fromstate A
will require one to go throughstate B
fromstate A
and then move the parcel alongstate B
to our final destinationstate C
. Even so, given that in our case the destination isstate C
why pass throughstate B
π« which doesn't need the parcel?One solution to avoid traveling through
state B
with the parcel fromstate A
in real life will be to use an airplane. This will usually need the departure airport and landing airport to accommodate the landing parcel.React context serves the same purpose as an airplane facility. In the case of React, the departure port is called the provider and the landing port is the consumer (Context.Provider and Context.Consumer).
Back to our code, move into your src folder, then create a context.js and withContext.js files.
$: cd src
$: touch context.js withContext.js
Paste this (or write it out if you don't mind) into the context.js file
//...src/context.js
import React from "react";
const Context = React.createContext();
// provide us the travelling facilities - Context Provider and Consumer
export const Provider = Context.Provider
// subscribe our components to the data
export const Consumer = Context.Consumer
// fetches the data
export default Context;
//...src/withContext.js
import React from 'react';
import { Context } from './context';
const withContext = (WrappedComponent) => () =>
(
<>
<Consumer>
{(context) => <WrappedComponent context={context} />}
</Consumer>
</>
);
export default withContext;
A quick detour on why we no longer need the withContext.js file
The primary reason for withContext.js is making a component subscribe to the context provider's values. Thanks to the reactuseContext
hook, this is no longer needed. Whatever the withContext.js file does, now comes built-into react π. Please clap for our caring react libraryπ.
Instead of the withContext.js file, I strongly recommend the use ofuseContext
hook, and we will be doing the same in this post. However, I will go on to show a use case with withContext we just created.
import withContext from "./withContext"
// thanks to the withContext wrapper, the value props
// in the Provider component, is now passed to
// our Example component as Props
function Example (props) {
const { state } = props
...
}
export default withContext(Example);
Oops, we forgot about our backend pardon me. I am only so engrossed into the frontend that I sometimes forget about our backend. So let's pause and quickly set up the backend. Thanks to json-server, it is super simple to do.
Fake Backend Setup
step I:
create a backend folder in your src folder and dump a JSON file that will contain our initial resource for the backend. Here is a link to the content in the ...src/backend/db.json.
//...src
$: mkdir backend && cd backend
$: touch db.json
// paste code from here: https://github.com/kelvinsekx/bitmama/blob/adjusted/src/backend/db.json
Step II
Finally, move into your package.json folder and paste the code below into your scripts object
//.../package.json
...
"scripts": {
...
"eject": "react-scripts eject",
"backend": "./node_modules/.bin/json-server-auth ./src/backend/db.json --port 3001"
},
...
Open your terminal and run.
npm run backend
If you paste, localhost:3001/products, into your browser, it should send a response of a JSON object.
Back to React: withContext
Dump the code into your App.js file
//...src/App.js
import React, { useState, useEffect, useRef } from 'react';
import {
Routes,
Route,
Link
} 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 { Provider } from './context';
export default function App() {
const [state, setState] = useState({
user: null,
cart: {},
products: [],
});
return (
<Provider
value={{
...state
}}
>
<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"
className="navbar-burger burger"
aria-label="menu"
aria-expanded="false"
data-target="navbarBasicExample"
onClick={(e) => {
e.preventDefault();
setState({ ...state, showMenu: !state.showMenu });
}}
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</label>
</div>
<div
className={`navbar-menu ${
state.showMenu ? 'is-active' : ''
}`}
>
<Link to="/products" className="navbar-item">
Products
</Link>
{state.user && 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(state.cart).length}
</span>
</Link>
{!state.user ? (
<Link to="/login" className="navbar-item">
Login
</Link>
) : (
<Link to="/" onClick={f=>f} className="navbar-item">
Logout
</Link>
)}
</div>
</nav>
<Routes>
<Route exact path="/" element={<ProductList />} />
<Route exact path="/login" element={<Login />} />
<Route exact path="/cart" element={<Cart />} />
<Route exact path="/add-product" element={<AddProduct />} />
<Route exact path="/products" element={<ProductList />} />
</Routes>
</div>
</Provider>
);
}
To complete the react-router-dom implementation, we need to place the BrowserRouter in the index.js file
//...src/index.js
...
import { BrowserRouter } from 'react-router-dom';
...
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
...
In summary, what we are doing in this file is to create pages and navigation functionalities with react-router and the nav component.
We wrapped everything in a Provider component (passed to us by the context) that allows its descendant components it is wrapped with to subscribe to it's value props so that when the value props changes, the provider re-renders the descendants in it.
Let's make a quick bootstrapped code for the four pages in the routes component:
$: mkdir components && cd components
$: touch addProduct.js cart.js login.js productList.js
// ...src/components/addProduct.js
export default function AddProduct (){
return <p>add product page</p>
}
// ...src/components/cart.js
export default function Cart (){
return <p>cart page</p>
}
// ...src/components/login.js
export default function Login (){
return <p>login page</p>
}
// ...src/components/productList.js
export default function ProductList (){
return <p>product list page</p>
}
Since you've gone this far, Oops, I forgot to mention to run the app in your terminal.
Please do that now, with $: npm run start
in the root of your react app.
So far so good, O boy! everything should work fine except for a few warnings saying the useEffect
and useRef
isn't defined. Irrespective, one should be able to navigate to each page with the nav on the home page.
Put context to use
Since every component is subscribed to the Provider's value props, why not confirm if those components can truly access the values.
To confirm our doubts, the cart page will be for a case study:
//...src/components/carts.js
import React, {useContext} from 'react'
import Context from "./context"
export default function Cart (){
const {user} = useContext(Context)
return <p>{user} : cart page</p>
}
You would remember that the user value in state is null, go replace it to any word and see the changes in action on the cart page.
//...src/app.js
export default function App() {
const [state, setState] = useState({
user: "Kelvinsekx",
cart: {},
products: [],
});
// ...rest of the code
Now that you have gotten a good hold on Context, we can move on to adding the core functionalities of our e-commerce site.
What our simple E-commerce site wants to do
- users visit the e-commerce site,
- users visit are directed to list of products page
- where he/she can add item to cart,
- if an unauthorized-user tries to go to the cart to check out,
- the unauthorized-user is forced to the login page (pardon me no signup page for now)
- after the user must have logged in, he/she can now return to the cart, pay and checkout
We can implement these functionalities and instructions next
Instruction 1:
users visits are directed to list of products page
Oops, we already took a step unconsciously by directing every home visit to the products page.
// ...src/app.js
//***
<Routes>
<Route exact path="/" element={<ProductList />} />
//...rest_of_codes
Step 2: where he/she can add item to cart
Presently, the productList component is as good as empty. We can update the it to show a list of all products together with the functionality to add each of them to cart.
//...src/components/productList.js
import React, { useContext } from 'react';
import ProductItem from './productItem';
import Context from './../context';
const ProductList = () => {
const { products, addToCart } = useContext(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={addToCart}
/>
))
) : (
<div className="column">
<span className="title has-text-grey-light">
No products found!
</span>
</div>
)}
</div>
</div>
</>
);
};
export default ProductList;
The component is simple enough. It gets a list of products and map them over the productItem component.
import React from 'react';
const ProductItem = ({ product, addToCart }) => {
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>
{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={() =>
addToCart({
name: product.name,
product,
amount: 1,
})
}
>
Add to Cart
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default ProductItem;
Instruction 2B: addToCart implementation
The ProductItem needs the functionality of addToCart - this will enable us to add each product to cart when clicked - let's go add that to our app.js file so that it is accessible in the productList.
//...src/app.js
const addToCart = (cartItem) => {
const clonedState = { ...state };
const { cart, products } = clonedState;
if (cart[cartItem.name]) {
const updatedProductIndex = findUpdatedStock(
cartItem.product.id
);
cart[cartItem.name].amount += cartItem.amount;
cart[cartItem.name].product.stock++;
products[updatedProductIndex].stock--;
} else {
cart[cartItem.name] = cartItem;
}
if (
cart[cartItem.name].amount > cart[cartItem.name].product.stock
) {
cart[cartItem.name].amount = cart[cartItem.name].product.stock;
}
localStorage.setItem('cart', JSON.stringify(cart));
setState({ ...clonedState, cart });
};
return (
<Provider value={{
...state,
addToCart
}}>
)
Now that we've been able to create the addToCart feature, let's move into the cart page and see how things look.
// ...src/components/cart.js
import React, { useContext } from 'react';
import Context from './../context';
import CartItem from './cartItem';
const Cart = () => {
const { cart, clearCart, checkout, removeFromCart } =
useContext(Context);
const cartKeys = 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]}
removeFromCart={removeFromCart}
/>
))}
<div className="column is-12 is-clearfix">
<br />
<div className="is-pulled-right">
<button
onClick={clearCart}
className="button is-warning "
>
Clear cart
</button>{' '}
<button
className="button is-success"
onClick={checkout}
>
Checkout
</button>
</div>
</div>
</div>
) : (
<div className="column">
<div className="title has-text-grey-light">
No item in cart!
</div>
</div>
)}
</div>
</>
);
};
export default Cart;
step 3: redirect user that isn't logged in upon checkout
On the cart page, I checked if there is an already selected product <div className="container">
{cartKeys.length ?
if no item is selected/in the cart object, it returns no Item in cart!
. The clearCart and checkout button are used to clear all selected products and move to checkout page respectively, the handlers:
//...src/app.js
const clearCart = () => {
let cart = {};
localStorage.removeItem('cart');
setState({...state, cart });
};
const checkout = () => {
if (!state.user) {
navigate('/login');
}
const cart = state.cart;
const products = 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;
});
setState({ ...state, products });
setTimeout(clearCart, 2000);
};
...
return (
<Provider value={{
...state,
addToCart,
clearCart,
checkOut
}}>
)
If a key exist, we then map it into a cartItem that fetches the cart information using the cartKey. Here is the cartItem component below.
import React from 'react';
const CartItem = ({ cartItem, cartKey }) => {
const { product, amount } = cartItem;
//...please copythe rest of the code
<div
className="media-right"
onClick={() => removeFromCart(cartKey)}
>
<span className="delete is-large"></span>
</div>
</div>
</div>
</div>
);
};
export default CartItem;
Our removeFromCart(carkKey is the most important feature to us here, because it helps us remove an item from the cart.
Finally that we've been able to put in the necessary features. We can add authorization, where only logged in user can checkout.
We would be adding a log in and log out feature, and create a login page as final steps.
Step 4: the unauthorized-user is forced to the login page (pardon me no signup page for now)
Create a login component in the components folder and paste the code from the repo: It works very similar to most our implemented components so no need to replicate it here.
However, the login page uses withContext component we created earlier so that you at least see it in action π
const login = async (email, password) => {
const res = await axios
.post('http://localhost:3001/login', { email: email, password })
.catch((res) => ({
status: 401,
message: 'Unauthorized',
err: res,
}));
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,
};
setState({ ...state, user });
localStorage.setItem('user', JSON.stringify(user));
return true;
} else {
return false;
}
};
const logout = (e) => {
e.preventDefault();
setState({ ...state, user: null });
localStorage.removeItem('user');
return navigate('/login');
};
...
return (
<Provider value={{
...state,
addToCart,
clearCart,
checkOut,
login,
logout
}}>
)
And there you have it everything is fine and shinny.
Also, I will like to seek a pardon, I would skip explaining the add product
implementations but you can read the code on the repo. Bye!!