Build a simple e-commerce site with react

Β·

13 min read

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.

recorded-e-commerce.gif

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

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

  1. Dive into your terminal and create a fresh shinny react app.
    $: npx create-react-app e-commerce
  2. 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 using json-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 to state C ?

To get to state C with a parcel from state A will require one to go through state B from state A and then move the parcel along state B to our final destination state C. Even so, given that in our case the destination is state C why pass through state B 😫 which doesn't need the parcel?

One solution to avoid traveling through state B with the parcel from state 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 react useContext 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 of useContext 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!!

Β