Build Amazon Clone with React, Context API, and Firebase
Amazon is a vast Internet-based enterprise that sells books, music, movies, housewares, electronics, toys, and many other goods. In this article, we will explore the react hooks and context API as we build the amazon clone using the react context API for state management within our app.
If you want to learn about React Native animation, then use this brilliant guide.
Pre-requisite
- Basic understanding of JavaScript ES6
- Basic understanding of HTML and CSS
- Have Nodejs installed on your machine
Technologies
- React
- Firebase
- Font awesome (icons)
Project overview
Our clone will allow users to register, login, add products to the shopping cart, remove product from the shopping cart. Our clone will permit only an authenticated user to access the home page.
Here is a live demo of our clone – https://amazonna.netlify.app/
Project setup
Let’s get started as we create a new project using the create-react-app so go to a directory where you will store this project and type the following in the terminal
create-react-app amazon-clone
The above command uses the create-react-app CLI tool to generate a react boilerplate project for us so that we don’t have to configure any tooling.
For the command above to work, the create-react-app CLI tool must be installed globally using the command below.
npm install -g create-react-app
Spin up the server with the following:
cd amazon-clone
npm start
We will delete some files that are not necessary for this project. These files include (App.css, App.test.js, logo.svg and registerServiceWorker.js)
Next, make the following change to the index.js file
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App/>, document.getElementById('root'));
Setting up Firebase
In this project, we will use firebase for user authentication.
Firebase is provides developers with servers, APIs and datastore, all written so generically that developers can modify it to suit most needs. It is user friendly
In this article we will use the firestore to store our data.
Head to fiarebase.google.com and sign up. It is free for small projects.
Next you will be directed to your firebase projects page like the following:
Click on the Add project button.
step 1 :
Add the name of your project (amazon-clone)
step 2 :
check enable Google Analytics for this project and click continue
step 3 :
select default account for firebase.
After the firebase project is successfully created, click on the web icon and follow the prompt to register your app.
Next Install Firebase CLI
npm install -g firebase-tools
and continue to console.
Next, click on the web icon and select the config option as follow:
Connect our project to firebase
Install firebase in the project
npm install firebase
Create a firebase.js file in the src folder with the following:
import firebase from "firebase";
const firebaseConfig = {
// paste your firebase config object here
}
const firebaseApp = firebase.initializeApp(firebaseConfig)
const auth = firebase.auth();
export { auth };
React Hooks
Since we will be writing functional based components, I’ll briefly discuss what react hooks are as we’ll be using some of them in this article.
According to the React Docs:
“Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.”
THE useState
HOOK
The useState hook allows us to use state in our functional components similar this.state
in class based component. A useState
hook takes the initial value of our state as the only argument, and it returns an array of two elements. The first element is our state variable and the second element is a function in which we can use the update the value of the state variable.
Let’s take a look at the following example:
import React, {useState} from "react";
function Counter(){
const [counter, setCounter] = useState(0);
}
Here, counter
is our state variable and its initial value is 0
while setCounter
is a function which we can use to update the value of count.
THE useContext
HOOK
This hook basically allows us to consume the value of a context. It Accepts a context object (the value returned from createContext
and returns the current context value for that context. For more information check https://reactjs.org/docs/hooks-reference.html
The Context API
According to the React documentation:
“Context provides a way to pass data through the component tree without having to pass props down manually at every level.”
In simpler terms, Context API provides a way for you to make particular data available to all components throughout the component tree no matter how deeply nested that component may be.
Now that we have an understanding of what the context API is, let’s create one for our project.
In the component folder, create a stateProvider folder containing StateProvider.js
with the following:
// setup data layer
// we need this to track the basket
import React, { createContext, useContext, useReducer } from "react";
//This is the data layer
export const StateContext = createContext()
//Build a provider
export const StateProvider = ({reducer, initialState, children}) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
)
//This is how we use it inside of a component
export const useStateValue = () => useContext(StateContext)
The StateProvider
will be used to wrap our entire app so that every component can access the StateContext
(ie the data layer).
Create a reducer.js file in the src folder with the following:
export const initialState = {
basket: [],
user: null
}
export const getBasketTotal = (basket) => basket?.reduce((amount, item) => item.price + amount, 0);
function reducer(state, action) {
switch(action.type) {
case "SET_USER":
return {
...state,
user: action.user
}
case "ADD_TO_BASKET":
return {
...state,
basket: [...state.basket, action.item]
};
case 'REMOVE_FROM_BASKET':
let newBasket = [...state.basket];
const index = state.basket.findIndex((basketItem) => basketItem.id === action.id);
if(index >= 0) {
//item exist in the basket, remove it
newBasket.splice(index, 1)
} else {
console.warn (
`Can't remove product{id: ${action.id}} as it is not in the basket`
);
}
return{
...state,
basket: newBasket
};
default:
return state
}
}
export default reducer;
Update App.js as follows:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { StateProvider } from '../src/components/stateProvider/StateProvider'
import reducer, { initialState } from './reducer';
ReactDOM.render(
<React.StrictMode>
<StateProvider initialState={initialState} reducer={reducer}>
<App />
</StateProvider>
</React.StrictMode>,
document.getElementById('root')
);
The StateProvider
is used to wrap our entire app so that every component can access the StateContext
(ie the data layer).
Components
Our amazon clone will consist of the following components:
- Header
- Login
- Product
- Subtotal
- Checkout
- Home
- CheckoutProduct In the src folder, create a folder called components with the following folders header, login,product,subtotal,checkout,home and checkoutProduct. We will install
react-router-dom
a dependency for routing in react apps,@material-ui/core
,@material-ui/icons
for icons. npm install react-router-dom
npm install @material-ui/core
npm install @material-ui/icons
Header Component
The header comprises of the amazon logo, search field, cart icon and the number of items in the user’s basket. If a user is logged in, he will have these Hello user@gmail.com
and Sign Out
text on his header component. Otherwise, he will have Hello
and Sign In
In the header folder, create Header.js file with the following:
import React from 'react'
import {Link, useHistory} from "react-router-dom"
import './header.css'
import SearchIcon from "@material-ui/icons/Search"
import ShoppingBasketIcon from "@material-ui/icons/ShoppingBasket"
import {useStateValue } from "../stateProvider/StateProvider"
import { auth } from 'firebase'
function Header() {
const [{basket, user}] = useStateValue();
const history = useHistory();
const login = () => {
if (user) {
auth().signOut();
history.push("/login")
}
}
return (
<nav className="header">
{/* logo on the left -> img */}
<Link to="/">
<img className="header__logo" src="http://pngimg.com/uploads/amazon/amazon_PNG11.png" alt="amazon logo"/>
</Link>
{/* search box */}
<div className="header__search">
<input type="text" className="header__searchInput"/>
<SearchIcon className="header__searchIcon"/>
</div>
{/* 3 links */}
<div className="header__nav">
<Link to={!user && "/login"} className="header__link">
<div onClick={login} className="header__option">
<span className="header__optionLineOne">Hello {user?.email}</span>
<span className="header__optionLineTwo">{user ? "Sign Out" : "Sign In"}</span>
</div>
</Link>
</div>
<div className="header__nav">
<Link to="/order" className="header__link">
<div className="header__option">
<span className="header__optionLineOne">Returns</span>
<span className="header__optionLineTwo">& Orders</span>
</div>
</Link>
</div>
<div className="header__nav">
<Link to="/login" className="header__link">
<div className="header__option">
<span className="header__optionLineOne">Your</span>
<span className="header__optionLineTwo">Prime</span>
</div>
</Link>
</div>
<Link to="/checkout" className="header__link">
<div className="header__optionBasket">
{/* shopping basket icon */}
<ShoppingBasketIcon/>
{/* number of items in the basket */}
{/* we use {basket?.length} to render the length of the basket when basket property of the state reaches the header component. without this, the dom will render the basket.length even when it has not reach the header component therby resulting to an error */}
<span className="header__optionLineTwo header__basketCount">{basket?.length}</span>
</div>
</Link>
{/* basket icon with number*/}
</nav>
)
}
export default Header
To access the basket and user object, we destructured them from the state in the context API as follows:
import {useStateValue } from "../stateProvider/StateProvider"
const [{basket, user}] = useStateValue();
In the header folder, create Header.css
file with the following:
.header{
background-color: #131921;
display: flex;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header__logo{
width: 100px;
margin: 0 20px;
margin-top: 18px;
object-fit: contain;
}
.header__optionBasket{
display: flex;
align-items: center;
}
.header__basketCount{
margin: 0px 10px;
}
.header__nav{
display: flex;
justify-content: space-evenly;
}
.header__option{
display: flex;
flex-direction: column;
margin: 0px 10px;
}
.header__optionLineOne{
font-size: 10px;
}
.header__optionLineTwo{
font-size: 13px;
font-weight: 800;
}
.header__link{
color: #ffffff;
text-decoration: none;
}
.header__search{
display: flex;
flex: 1;
}
.header__searchInput{
padding: 10px;
height: 12px;
border: none;
width: 100%;
}
.header__searchIcon{
background-color: #cd9042;
padding: 5px;
height: 22px !important;
}
Home Component
This component imports products data from products and pass it to the product component as props. In the home folder, create Home.js file with the following:
import React from 'react'
import "./Home.css"
import Product from "../product/Product.js"
import { products,bannerImg } from '../../products'
function Home() {
return (
<div className="home">
<img className="home__image" src={bannerImg} alt=""/>
{/*product id, title, price, rating */}
<div className="home__row">
{products.map(item => {
return (
<Product
id={item.id}
title={item.title}
image={item.image}
price={item.price}
rating={item.rating}
/>
)
})}
</div>
</div>
)
}
export default Home
In the home folder, create Home.css file with the following:
.home {
max-width: 1500px;
margin: 0px auto;
}
.home__row {
display: flex;
z-index: 1;
margin: 0px 5px;
flex-wrap: wrap;
margin: 0px auto;
justify-content: center;
}
.home__image{
width: 100%;
object-fit: contain;
mask-image: linear-gradient(to bottom, rgba(0,0, 0, 0), rgba(0, 0,0, 1));
margin-bottom: -150px;
}
products.js
This is the source of all the product data for our project.
In the src folder create products.js
export const products = [
{
id: 1,
title: "Oculus Quest All-in-one VR Gaming Headset – 64GB",
image: "https://images-na.ssl-images-amazon.com/images/I/31pEe2taIPL._AC_US327_FMwebp_QL65_.jpg",
price: 11.96,
rating: 4
},
{
id: 2,
title: "Nintendo Switch with Neon Blue and Neon Red Joy‑Con - HAC-001(-01)",
image: "https://images-na.ssl-images-amazon.com/images/I/41DQoLIfsRL._AC_US327_FMwebp_QL65_.jpg",
price: 15.96,
rating: 3
},
{
id: 3,
title: "Xbox game",
image: "https://images-na.ssl-images-amazon.com/images/G/01/amazonglobal/images/email/asins/DURM-2B5ECC8E3DA42415._V531815325_.jpg",
price: 23.96,
rating: 5
},
{
id: 4,
title: "The LeanStartup2: How constant innovative creators",
image: "https://images-na.ssl-images-amazon.com/images/I/51T-sMqSMiL._SX329_BO1,204,203,200_.jpg",
price: 9.96,
rating: 4
},
{
id: 5,
title: "Play station game pad",
image: "https://images-na.ssl-images-amazon.com/images/G/01/amazonglobal/images/email/asins/DURM-2B638E86650FFF18._V531815327_.jpg",
price: 19.96,
rating: 5
}
]
export const bannerImg = "https://images-eu.ssl-images-amazon.com/images/G/02/digital/video/merch2016/Hero/Covid19/Generic/GWBleedingHero_ENG_COVIDUPDATE__XSite_1500x600_PV_en-GB._CB428684220_.jpg"
Product component
This component receives the product’s information as props, and displays it on the browser.
In the product folder, create Product.js
file with the following:
import React from 'react'
import "./Product.css"
import { useStateValue } from "../stateProvider/StateProvider"
function Product({id, title, price, rating, image}) {
const [{}, dispatch] = useStateValue();
const addToBasket = () => {
//Add item to basket...
dispatch({
type: "ADD_TO_BASKET",
item: {
id,
title,
image,
price,
rating
}
})
};
return (
<div className="product">
<div className="product__info">
<p>{title}</p>
<p className="product__price">
<small>$</small>
<strong>{price}</strong>
</p>
<div className="product__rating">
{Array(rating).fill().map((index) => (
<i key={index} class="fa fa-star"></i>
))}
</div>
</div>
<img src={image} alt=""/>
<button onClick={addToBasket}>Add to basket</button>
</div>
)
}
export default Product
Add Products to Basket
In order to add product to the basket, the context API provides us with the dispatch method which dispatches actions to the reducer. and it is used as follows:
const [{}, dispatch] = useStateValue();
const addToBasket = () => {
//Add item to basket...
dispatch({
type: "ADD_TO_BASKET",
item: {
id,
title,
image,
price,
rating
}
})
};
Whenever the dispatch method is invoked, the reducer checks the type property of the action and updates the state accordingly.
Let’s take a look at what the reducer does with this dispatched object.
...
function reducer(state, action) {
switch(action.type) {
...
case "ADD_TO_BASKET":
return {
...state,
basket: [...state.basket, action.item]
};
...
default:
return state
}
}
...
Since the type of the object(action) which we dispatched corresponds with the case "``ADD_TO_BASKET``"
, the reducer updates the state with the new object that is added to the basket.
In the product folder, create Product.css file with the following:
.product {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 25%;
max-height: 400px;
min-width: 100px;
background-color: white;
margin: 10px;
padding: 20px;
z-index: 1;
}
.product__info {
height: 100px;
margin-bottom: 15px;
align-self: flex-start;
}
.product__price {
margin-top: 5px;
}
.product__rating{
display: flex;
}
.product > img {
width: 100%;
max-height: 200px;
object-fit: contain;
margin-bottom: 15px;
}
.fa{
color: #f0c14b;
}
.product > button {
background-color: #f0c14b;
border: 1px solid;
border-color: #a88734 #9c7e31 #846a29;
}
CheckoutProduct Component
This component receives the product’s information from the checkout component as props, and displays it on the browser. It also allows the user to remove products from the basket.
In checkoutProduct
folder, create CheckoutProduct.js
file with the following:
import React from 'react'
import "./checkoutProduct.css"
import {useStateValue} from "../stateProvider/StateProvider"
function CheckoutProduct({id, image, title, price, rating}) {
const [{}, dispatch] = useStateValue();
const removeFromBasket = () => {
// remove from basket
dispatch({
type: "REMOVE_FROM_BASKET",
id
})
}
return (
<div className="checkoutProduct">
<img src={image} alt=""/>
<div className="checkoutProduct__info">
<p className="checkoutProduct__title">{title}</p>
<p className="checkoutProduct__price">
<small>$</small>
<strong>{price}</strong>
</p>
<div className="checkoutProduct__rating">
{Array(rating).fill().map((index) => (
<p key={index}>star</p>
))}
</div>
<button onClick={removeFromBasket}>Remove from basket</button>
</div>
</div>
)
}
export default CheckoutProduct
In checkoutProduct
folder, create CheckoutProduct.css
file with the following:
.checkoutProduct{
display: flex;
margin: 20px 0px;
}
.checkoutProduct__image {
object-fit: contain;
width: 100px;
height: 180px;
}
.checkoutProduct__rating {
display: flex;
}
.checkoutProduct__info {
padding-left: 20px;
}
.checkoutProduct__title {
font-size: 17px;
font-weight: 800;
}
.checkoutProduct__info > button {
background: #f0c14b;
border: 1px solid;
margin-top: 10px;
border-color: #a88734 #9c7e31 #846a29;
color: #111;
}
Remove Products from Basket
In order to remove products from the basket, we use the dispatch method to dispatch an action with type REMOVE_FROM_BASKET
to the reducer as follows:
const removeFromBasket = () => {
// remove from basket
dispatch({
type: "REMOVE_FROM_BASKET",
id
})
}
The reducer checks for the case that matches the type property of the action and updates the state accordingly.
...
case 'REMOVE_FROM_BASKET':
// Logic for removing item from basket
//clone the basket
let newBasket = [...state.basket];
const index = state.basket.findIndex((basketItem) => basketItem.id === action.id);
if(index >= 0) {
//item exist in the basket, remove it
newBasket.splice(index, 1)
} else {
console.warn (
`Can't remove product{id: ${action.id}} as it is not in the basket`
);
}
return{
...state,
basket: newBasket
};
...
SubTotal Component
This component displays the total amount of products added to the basket.
we will install react-currency-format
to format the prices of products in dollars.
import React from 'react'
import CurrencyFormat from 'react-currency-format';
import {useStateValue} from "../stateProvider/StateProvider"
import "./Subtotal.css"
import { getBasketTotal } from '../../reducer';
function Subtotal() {
const [{basket}, dispatch] = useStateValue();
return (
<div className="subtotal">
{/* price */}
<CurrencyFormat
renderText = {(value) => (
<>
<p>
( Subtotal {basket.length} items ) : <strong>{`${value}`}</strong>
</p>
<small className="subtotal__gift">
<input type="checkbox"/> This order contains
</small>
</>
)}
value={getBasketTotal(basket)}
displayType={'text'}
thousandSeparator={true}
prefix={'$'}
/>
<button>Proceed to checkout</button>
</div>
)
}
export default Subtotal
In the subTotal
component, create SubTotal.css
with the following:
.subtotal {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 300px;
height: 100px;
padding: 20px;
background-color: #f3f3f3;
border-radius: 3px;
border: 1px solid #dddddd;
}
.subtotal > button {
background: #f0c14b;
border: 1px solid;
margin-top: 10px;
border-color: #a88734 #9c7e31 #846a29;
color: #111;
}
Checkout Component
This component displays the list of products currently in the basket and the SubTotal
component. It retrieves all the product data from the basket and passes it down to the CheckoutProduct
component as props.
import React from 'react'
import { useStateValue } from "../stateProvider/StateProvider"
import "./Checkout.css"
import CheckoutProduct from "../checkoutProduct/CheckoutProduct"
import Subtotal from "../subtotal/Subtotal"
function Checkout() {
const [{ basket }] = useStateValue()
return (
<div className="checkout">
<div className="checkout__left">
<img className="checkout__ad" src="https://images-na.ssl-images-amazon.com/images/G/01/AmazonExports/Fuji/2020/May/Hero/Fuji_TallHero_45M_v2_1x._CB432458380_.jpg" alt="ad"
/>
{basket?.length === 0 ? (
<div>
<h2>Your shopping basket is empty</h2>
<p>
You have no items in your basket. To buy one or add item to basket click the add to basket button
</p>
</div>
) : (
<div>
<h2 className="checkout__title">Your shopping basket</h2>
{basket.map(item => {
console.log(item)
return (
<CheckoutProduct
id={item.id}
title={item.title}
image={item.image}
price={item.price}
rating={item.rating}
/>
)
})}
</div>
)}
</div>
{basket?.length > 0 &&
<div className="checkout__right">
<Subtotal/>
</div>
}
</div>
)
}
export default Checkout
In the checkout
folder, create Checkout.css
with the following:
.checkout {
display: flex;
padding: 20px;
background-color: white;
height: max-content;
}
.checkout__title {
margin-right: 10px;
padding: 10px;
border-bottom: 1px solid lightgray;
}
.checkout__ad {
width: 100%;
height: 100px;
margin-bottom: 10px;
}
Login Component
This component uses the firebase auth module to sign up new users and also verify the sign up users each time they sign in.
In login folder create Login.js
component with the following:
import React, {useState} from 'react'
import "./Login.css"
import {Link, useHistory} from "react-router-dom"
import {auth} from "../../firebase"
function Login() {
const history = useHistory()
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const signIn = (event) => {
event.preventDefault()
auth.signInWithEmailAndPassword(email, password)
.then( auth => {
//redirect to home page
history.push("/")
})
.catch(err => {
alert(err.message)
})
}
const register = (event) => {
event.preventDefault()
auth.createUserWithEmailAndPassword(email, password)
.then(auth => {
//create a user, login and redirect to homepage
history.push("/")
})
.catch(err => {
alert(err.message)
})
}
return (
<div className="login">
<Link to="/">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Amazon_logo.svg/1024px-Amazon_logo.svg.png" alt="" className="login__logo"/>
</Link>
<div className="login__container">
<h1>Sign in</h1>
<form>
<h5>Email:</h5>
<input value={email} onChange={event => setEmail(event.target.value)} type="email"/>
<h5>Password:</h5>
<input value={password} onChange={event => setPassword(event.target.value)} type="password"/>
<button type="submit" onClick={signIn} className="login__signInBtn">sign in</button>
<p>
by signing in you agree to amazon condition of use and sale.Please see our privacy notice, our cookies notice and our interest based ad notice.
</p>
<button onClick={register} className="login__registerBtn">create your amazon account</button>
</form>
</div>
</div>
)
}
export default Login
In login folder create Login.css
component with the following:
.login {
background-color: white;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.login__container {
width: 300px;
margin: 0px auto;
display: flex;
flex-direction: column;
padding: 20px;
border: 1px solid lightgray;
}
.login__container > h1 {
font-weight: 500;
margin-bottom: 20px;
}
.login__container > form > h5 {
margin-bottom: 5px;
}
.login__container > form > input {
height: 30px;
width: 98%;
background-color: white;
margin-bottom: 10px;
}
.login__container > form > p {
margin-top: 15px;
font-size: 12px;
}
.login__logo {
width: 100px;
object-fit: contain;
margin: 20px 0px;
}
.login__signInBtn {
width: 100%;
background: #f0c14b;
border: 1px solid;
padding: 10px 0px;
margin-top: 10px;
border-color: #a88734 #9c7e31 #846a29;
color: #111;
}
.login__registerBtn {
width: 100%;
background: lightgrey;
border: 1px solid;
padding: 10px 0px;
margin-top: 10px;
border-color: lightgrey;
color: #111;
}
App component
When ever a user logs in or signs up, an action should be dispatched to the reducer to update the user property of the state with the authenticated user’s data. Furthermore, whenever a user logs out, an action should be dispatched to the reducer to set the user property of the state to a value of null.
Finally we need to update app.js with the as follows:
import React, { useEffect } from 'react';
import './App.css';
import Home from "./components/home/Home"
import {
BrowserRouter as Router,
Switch,
Route,
} from "react-router-dom";
import Header from "./components/header/Header"
import Checkout from './components/checkout/Checkout';
import Login from './components/login/Login';
import {useStateValue} from "./components/stateProvider/StateProvider"
import {auth} from "./firebase"
function App() {
const [{user}, dispatch] = useStateValue();
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(authUser => {
if(authUser) {
//The user is logged in
dispatch({
type: "SET_USER",
user: authUser
})
} else {
//The user is logged out
dispatch({
type: "SET_USER",
user: null
})
}
})
return () => {
// Any clean up operation goes in here
unsubscribe();
}
}, [])
return (
<Router>
<div className="App">
<Switch>
<Route path="/login">
<Login/>
</Route>
<Route path="/checkout">
<Header/>
<Checkout/>
</Route>
<Route path="/">
<Header/>
<Home/>
</Route>
</Switch>
</div>
</Router>
);
}
export default App;
Now we can checkout the features we have implemented so far in our amazon clone
npm start
Conclusion
That’s it! We have succeeded in building our Amazon clone using the Context API for state management and firebase for user authentication.
In the process, we have learned:
- What the Context API is and the problem it solves;
- When to use the Context API;
- Creating
Context
and consuming it functional components. - What React Hook is
- How to setup firebase authentication.
You can find the complete project for this article on Github. Happy coding.