Learn how React Context API works by Building a Minimal Ecommerce Shopping App
Tags: javascript, react, beginners, tutorial
Published: 2022-05-20 10:14:00

So, this is a project that has been on my mind for a while, but I didn't put much thought or effort into building it.

Then [Hashnode's Writeathon](https://townhall.hashnode.com/the-epic-hashnode-writeathon?source=hashnode_feed) came up, and I thought, this is the perfect opportunity to write this article that will both help me improve my React knowledge and also help other developers who are learning about it for the first time or want to brush up on their knowledge of the subject. Win-win situation!

In this article, you'll learn about the React Context API, how it solves prop drilling, and how I built this simple shopping app with the following features:

- Store current items
- Update the context when the user clicks on the "Add to Cart" button
- Display the cart count in the Navigation bar
- Add and remove items from the Cart
- Save cart items to local storage

Below is a screenshot of what we'll be building:

![Fashion store screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3sfr3fvvjpklgmxaw5kl.png)

If that looks good, let's get started!

## Prerequisites

This article assumes that you have :

- A basic knowledge of HTML, CSS, JavaScript, and React.
- [Node](https://nodejs.org/en/) and [npm](https://www.npmjs.com/) installed on your local development machine.
- Code Editor(VS Code)

## Overview of React Context

### What is React Context?

React Context is a method used to pass data(and functions) from parent to child component(s), by storing the data in a store (similar to [Redux](https://redux.js.org/)), from where you can easily access and import the data into whatever components you choose.

This is a better alternative to _prop drilling_, which is the term used to describe the passing of data through several layers of components, even if those components have no actual need for the data.

### When to use Context?

Context is designed to share data that can be considered "global" to the whole app. An example would be the currently authenticated user, a theme, or user preferences(for example, language or locale).

> "Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult."
>
> Source: [Official Docs](https://reactjs.org/docs/context.html#when-to-use-context)

## Building the eCommerce Web app

### Illustrations

Before we get into the code, let’s look at the component hierarchy to better understand the relationship between the components of the app.

The illustration below shows how data will be passed down from the root component level (`App`) to the component rendering what is to be displayed(`items`).

![Prop Drilling representation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zquwnbbylst6hafuqqua.png)

However, what we'll be using in our app is what Context solves:

![With Context API](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d8c40euj7hip3lnpxjoe.png)

As you can see, the Context is like a store in your application. And once it is set up, you can simply import it into whatever component needs that data.

Now that we have gone through a basic overview of React Context, let's jump right into the project.

> Here is the [live demo](https://shop-context.netlify.app/) of what we'll be building, and if you also want to see the code, it's available on [Github](https://github.com/israelmitolu/Learn-Context-by-Building-a-Shopping-Website).

### Project Set up

Let's start by creating a new React project. I will be using Vite in this tutorial. If you haven't heard about it, do well to check out my [previous article](https://israelmitolu.hashnode.dev/why-you-should-ditch-create-react-app-for-vite) on it.

Of course, feel free to use your bundler of choice: Vite or CRA.

```node
# vite
npm init vite@latest react-shopping-cart --template react

# create react app
npx create-react-app react-shopping-cart
```

Once it is finished, run:

```node
cd react-shopping-cart
npm install
```

Dependencies we'll be using:

- [React Router](https://reactrouter.com/docs/en/v6)

```node
npm install react-router-dom@6
```

- [Styled components](https://styled-components.com/)

```node
npm install --save styled-components
```

> Note: We won’t be covering the styling to keep our code minimal; this is just to explain how the app works.
>
> Also, I have included some comments in the code so that you understand their purpose.

### Context Setup

In complex applications where the need for context is usually necessary, there can be multiple contexts, with each having its data and functions relating to the set of components that requires those data and functions.

For example, there can be a `ProductContext` for handling the components which use product-related data, and another `ProfileContext` for handling data related to authentication and user data.

However, for the sake of keeping things as simple as possible, we’ll use just one context instance.

In the `src` directory, create three folders: `Context`, `components` and `pages`.

Inside the `Context` folder create another folder, `Cart`.

Navigate to the `Cart` folder and add the following to a new file, `CartTypes.js`.

```javascript
// /src/Context/Cart/CartTypes.js`:

export const ADD_TO_CART = "ADD_TO_CART";
export const REMOVE_ITEM = "REMOVE_ITEM";
export const INCREASE = "INCREASE";
export const DECREASE = "DECREASE";
export const CHECKOUT = "CHECKOUT";
export const CLEAR = "CLEAR";
```

Here, we are defining the action types that our Context should have, and exporting them to be used within the Context.

Next, add the following to a new file, `CartContext.jsx` in the same directory to create the context:

```javascript
import { createContext } from "react";

const CartContext = createContext();

export default CartContext;
```

Next, create a new file `CartState.jsx` inside the `Cart` folder. Add the following code:

```javascript
import { useReducer } from "react";
import CartContext from "./CartContext";
import CartReducer from "./CartReducer";
import { sumItems } from "./CartReducer";

const CartState = ({ children }) => {
  //   Initial State of the cart
  const initialState = {
    cartItems: [],
    checkout: false,
  };

  //Set up the reducer
  const [state, dispatch] = useReducer(CartReducer, initialState);

  //Function to handle when an item is added from the store into the Cart
  const addToCart = (payload) => {
    dispatch({ type: "ADD_TO_CART", payload });
  };

  //Function to handle when an item that is in the cart is added again
  const increase = (payload) => {
    dispatch({ type: "INCREASE", payload });
  };

  //Function to handle when an item is removed from the cart
  const decrease = (payload) => {
    dispatch({ type: "DECREASE", payload });
  };

  //Function to remove an item from the cart
  const removeFromCart = (payload) => {
    dispatch({ type: "REMOVE_ITEM", payload });
  };

  //Function to clear the cart
  const clearCart = () => {
    dispatch({ type: "CLEAR" });
  };

  //Function to handle when the user clicks the checkout button
  const handleCheckout = () => {
    dispatch({ type: "CHECKOUT" });
  };

  return (
    //Add the functions that have been defined above into the Context provider, and pass on to the children
    <CartContext.Provider
      value={{
        showCart: state.showCart,
        cartItems: state.cartItems,
        addToCart,
        removeFromCart,
        increase,
        decrease,
        handleCheckout,
        clearCart,
        ...state,
      }}
    >
      {children}
    </CartContext.Provider>
  );
};

export default CartState;
```

Let's break the above code into bits.

First, the [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer) hook that is imported, accepts a reducer of type `(state, dispatch) => newState`, which then returns the current state. We also import the context files: `CartContext` and `CartReducer`.

Second, the `initialItems` is an array that defines the initial state of the cart when the page is loaded.

Third, in the `CartContext.Provider`, will render all of the props passed into it and will pass it through its `children`.

How the provider works is that the current context value is determined by the `value` prop of the nearest `<CartContext.Provider>`, and when it updates, the `useContext` hook will trigger a rerender with the latest context value passed to the `CartContext` provider.

Next, create a new file `CartReducer.jsx`, and add the following code:

```jsx
// /src/Context/Cart/CartReducer.jsx

//Import the Action types
import {
  REMOVE_ITEM,
  ADD_TO_CART,
  INCREASE,
  DECREASE,
  CHECKOUT,
  CLEAR,
} from "./CartTypes.js";

// Export function to calculate the total price of the cart and the total quantity of the cart
export const sumItems = (cartItems) => {
  Storage(cartItems);
  let itemCount = cartItems.reduce(
    (total, product) => total + product.quantity,
    0
  );
  let total = cartItems
    .reduce((total, product) => total + product.price * product.quantity, 0)
    .toFixed(2);
  return { itemCount, total };
};

// The reducer is listening for an action, which is the type that we defined in the CartTypes.js file
const CartReducer = (state, action) => {
  // The switch statement is checking the type of action that is being passed in
  switch (action.type) {
    // If the action type is ADD_TO_CART, we want to add the item to the cartItems array
    case ADD_TO_CART:
      if (!state.cartItems.find((item) => item.id === action.payload.id)) {
        state.cartItems.push({
          ...action.payload,
          quantity: 1,
        });
      }

      return {
        ...state,
        ...sumItems(state.cartItems),
        cartItems: [...state.cartItems],
      };

    // If the action type is REMOVE_ITEM, we want to remove the item from the cartItems array
    case REMOVE_ITEM:
      return {
        ...state,
        ...sumItems(
          state.cartItems.filter((item) => item.id !== action.payload.id)
        ),
        cartItems: [
          ...state.cartItems.filter((item) => item.id !== action.payload.id),
        ],
      };

    // If the action type is INCREASE, we want to increase the quantity of the particular item in the cartItems array
    case INCREASE:
      state.cartItems[
        state.cartItems.findIndex((item) => item.id === action.payload.id)
      ].quantity++;
      return {
        ...state,
        ...sumItems(state.cartItems),
        cartItems: [...state.cartItems],
      };

    // If the action type is DECREASE, we want to decrease the quantity of the particular item in the cartItems array
    case DECREASE:
      state.cartItems[
        state.cartItems.findIndex((item) => item.id === action.payload.id)
      ].quantity--;
      return {
        ...state,
        ...sumItems(state.cartItems),
        cartItems: [...state.cartItems],
      };

    // If the action type is CHECKOUT, we want to clear the cartItems array and set the checkout to true
    case CHECKOUT:
      return {
        cartItems: [],
        checkout: true,
        ...sumItems([]),
      };

    //If the action type is CLEAR, we want to clear the cartItems array
    case CLEAR:
      return {
        cartItems: [],
        ...sumItems([]),
      };

    //Return the state if the action type is not found
    default:
      return state;
  }
};

export default CartReducer;
```

Now that we're done setting up the context, the next thing will be to wrap the `App` inside the `Context`.

To do that, navigate to the `main.jsx`(Vite) or `index.js`(CRA) in the root directory. Add the following code:

```jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import CartState from "./Context/Cart/CartState";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <CartState>
      <App />
    </CartState>
  </React.StrictMode>
);
```

So, now our entire app has access to the Context.

### Building out the Components

For the `App.jsx`, we'll add the code that handles the application’s navigation.

```jsx
import Navbar from "./components/Navbar";
import Store from "./pages/Store";
import About from "./pages/About";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Cart from "./pages/Cart";

function App() {
  return (
    <>
      <BrowserRouter>
        <Navbar />
        <Routes>
          <Route path="/" element={<Store />} />
          <Route exact path="/about" element={<About />} />
          <Route exact path="/cart" element={<Cart />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}

export default App;
```

Now, let's create the components we'll need for our app's basic navigation to function properly.

Create a new file `Navbar.jsx` inside the `components` folder, and add the following:

```javascript
// General
import { useState, useEffect } from "react";
import { Link, NavLink } from "react-router-dom";
import CartIcon from "/assets/icons/cart.svg";
import styled from "styled-components";
import CartContext from "../Context/Cart/CartContext";
import { useContext } from "react";

const Navbar = () => {
  const [toggle, setToggle] = useState(false);
  const [innerWidth, setInnerWidth] = useState(window.innerWidth);

  // Get Screen Size
  useEffect(() => {
    const changeWidth = () => {
      setInnerWidth(window.innerWidth);
    };

    window.addEventListener("resize", changeWidth);

    return () => {
      window.removeEventListener("resize", changeWidth);
    };
  }, []);

  // Extract itemscount from CartContext
  const { cartItems } = useContext(CartContext);

  return (
    <Nav>
      <NavContainer>
        <Left>
          <Link to={"/"}>FASHION.</Link>
        </Left>

        <Right>
          <NavRightContainer
            style={{
              transform:
                innerWidth <= 500
                  ? toggle && "translateY(100vh)"
                  : "translateY(0%)",
            }}
          >
            <NavList>
              <NavItem>
                <NavLink to="/" onClick={() => setToggle(!toggle)}>
                  Store
                </NavLink>
              </NavItem>
              <NavItem>
                <NavLink to="/about" onClick={() => setToggle(!toggle)}>
                  About
                </NavLink>
              </NavItem>
              <NavItem>
                <a href="https://twitter.com/israelmitolu" target="_blank">
                  Contact
                </a>
              </NavItem>
              <NavItem>
                <Link to="/cart" onClick={() => setToggle(!toggle)}>
                  <p>Cart</p>
                  <NavCartItem>
                    <img src={CartIcon} alt="Shopping cart" />
                    {/* If the number of cartItems is greater than 0, display the
                    number of items in the cart */}
                    {cartItems.length > 0 && (
                      <CartCircle>{cartItems.length}</CartCircle>
                    )}
                  </NavCartItem>
                </Link>
              </NavItem>
            </NavList>
          </NavRightContainer>

          <MenuBtn onClick={() => setToggle(!toggle)}>
            <span></span>
            <span></span>
            <span></span>
          </MenuBtn>
        </Right>
      </NavContainer>
    </Nav>
  );
};
```

The above code sets up the Navigation bar, which will look like this:

![Navigation Bar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5xz1ldj8pllkyj930y37.PNG)

In the `pages` folder, which is in the `src` directory, create `Store.jsx`, `Cart.jsx` and `About.jsx`.

For the `Store.jsx`,

```jsx
import { products } from "../data";
import styled from "styled-components";
import ProductCard from "../components/ProductCard";

const Store = () => {
  return (
    <>
      <Heading>
        <h1>Browse the Store!</h1>
        <p>New Arrivals for you! Check out our selection of products.</p>
      </Heading>
      <ProductsContainer>
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </ProductsContainer>
    </>
  );
};

export default Store;
```

The `Store` contains the Product Cards, which are generated dynamically by mapping through the available `products` array which is exported from the `data.js` file:

```js
export const products = [
  {
    id: 1,
    name: "Cerveza Modelo",
    price: 919.11,
    image: "/assets/img/1.png",
  },
  {
    id: 2,
    name: "Diesel Life",
    price: 1257.92,
    image: "/assets/img/2.png",
  },
  {
    id: 3,
    name: "Indian Cricket Team jersey",
    price: 1500.85,
    image: "/assets/img/3.png",
  },
  {
    id: 4,
    name: "One Punch man - OK",
    price: 1250.9,
    image: "/assets/img/4.png",
  },
  {
    id: 5,
    name: "Hiking jacket",
    price: 1750.85,
    image: "/assets/img/5.png",
  },
  {
    id: 6,
    name: "Real Heart",
    price: 3100.61,
    image: "/assets/img/6.png",
  },
  {
    id: 7,
    name: "Fredd - Black and White",
    price: 1801.1,
    image: "/assets/img/7.png",
  },
  {
    id: 8,
    name: "Star Wars - The Last",
    price: 1199.99,
    image: "/assets/img/8.png",
  },
  {
    id: 9,
    name: "Yellow Blouse",
    price: 2395.16,
    image: "/assets/img/9.png",
  },
  {
    id: 10,
    name: "Rick and Morty - Supreme",
    price: 1243.82,
    image: "/assets/img/10.png",
  },
];

```

The `ProductCard` component shows the product details for each product.

Note that we would import useContext and CartContext in all the components where we need the data that is stored in the context.

The `onClick` events in the buttons handle the `addToCart` and `increase` functions which we have extracted from the CartContext:

```jsx
import styled from "styled-components";
import { Link } from "react-router-dom";
import { formatCurrency } from "../utils";
import CartContext from "../Context/Cart/CartContext";
import { useContext } from "react";

const ProductCard = ({ product }) => {
  // Extract these functions from the CartContext
  const { addToCart, increase, cartItems, sumItems, itemCount } =
    useContext(CartContext);

  //Check whether the product is in the cart or not
  const isInCart = (product) => {
    return !!cartItems.find((item) => item.id === product.id);
  };

  return (
    <CardWrapper>
      <ProductImage
        src={product.image + "?v=" + product.id}
        alt={product.name}
      />
      <ProductName>{product.name}</ProductName>
      <ProductCardPrice>{formatCurrency(product.price)}</ProductCardPrice>
      <ProductCardButtons>
        {isInCart(product) && (
          <ButtonAddMore
            onClick={() => {
              increase(product);
            }}
            className="btn"
          >
            Add More
          </ButtonAddMore>
        )}

        {!isInCart(product) && (
          <Button onClick={() => addToCart(product)}>Add to Cart</Button>
        )}
      </ProductCardButtons>
    </CardWrapper>
  );
};
```

For the code below, we will extract the state and functions that we need for the `Cart` component, which are: `cartItems`, `checkout` and `clearCart`.

Then, if there are any items in the `cartItems` array, render the items as `CartItem` components to the page:

```jsx
import CartItem from "../components/CartItem";
import { useContext } from "react";
import CartContext from "../Context/Cart/CartContext";
import styled from "styled-components";
import Checkout from "../components/Checkout";
import { Link } from "react-router-dom";

const Cart = () => {
  // Extract the functions from the Context
  const { cartItems, checkout, clearCart } = useContext(CartContext);

  return (
    <>
      <Heading>
        <h1>
          Shopping Cart
          <span>({cartItems.length})</span>
        </h1>
      </Heading>

      {/* Show the checkout message when the Checkout Button has been clicked */}
      {checkout && (
        <CheckoutMsg>
          <h4>Thank you for your purchase!</h4>
          <p>
            Your order has been placed and will be delivered to you within 24
            hours.
          </p>
          <Link to="/">
            <ShopBtn onClick={clearCart}>Continue Shopping</ShopBtn>
          </Link>
        </CheckoutMsg>
      )}

      <Layout>
        <div>
          {
            <CartItemWrapper>
              {/* If cart is empty, display message, and if not, display each cart
            Item in cart: {cartItems.length} */}
              {cartItems.length === 0 ? (
                <h4 style={{}}>Cart is empty</h4>
              ) : (
                <ul>
                  {cartItems.map((product) => (
                    <CartItem key={product.id} product={product} />
                  ))}
                </ul>
              )}
            </CartItemWrapper>
          }
        </div>

        <div>
          {/* Checkout component  */}
          {cartItems.length > 0 && <Checkout />}
        </div>
      </Layout>
    </>
  );
};
```

The `CartItem` component contains the items that are present in the current state. And, we'll extract some functions from the `CartContext`, namely: `removeFromCart`, `increase` and `decrease`:

```jsx
import { useContext } from "react";
import CartContext from "../Context/Cart/CartContext";
import styled from "styled-components";
import { formatCurrency } from "../utils";
import TrashIcon from "/assets/icons/trash-outline.svg";
import Plus from "/assets/icons/add-circle-outline.svg";
import Minus from "/assets/icons/remove-circle-outline.svg";

const CartItem = ({ product }) => {
  const { removeFromCart, increase, decrease } = useContext(CartContext);

  return (
    <SingleCartItem>
      <CartImage src={product.image} alt={product.name} />
      <div>
        <h5>{product.name}</h5>
        <p>{formatCurrency(product.price)}</p>
      </div>

      {/* Buttons */}
      <BtnContainer>
        <button
          onClick={() => increase(product)}
          className="btn btn-primary btn-sm mr-2 mb-1"
        >
          <Icon src={Plus} alt="" />
        </button>

        <div>
          <p>Qty: {product.quantity}</p>
        </div>

        {/* Display a minus icon or trash/delete icon based on the quantity of a particular product is in the cart */}
        {product.quantity > 1 && (
          <button onClick={() => decrease(product)} className="btn">
            <Icon src={Minus} alt="" />
          </button>
        )}

        {product.quantity === 1 && (
          <button onClick={() => removeFromCart(product)} className="btn">
            <Icon src={TrashIcon} alt="" />
          </button>
        )}
      </BtnContainer>
    </SingleCartItem>
  );
};
```

### Adding Cart Management

Now that we can add, remove, and display products, the final thing to do is implement our cart management. We have already initialised the cart as an empty array in `CartState.jsx`, meaning that once we restart the app, it will revert to being empty.

Now, what we will do is make sure that we load the existing cart from the local storage on component load.

Update the `initialState` method in `CartState.jsx` as follows:

```js
const initialState = {
  cartItems: storage,
  ...sumItems(storage),
  checkout: false,
};
```

Next, we need to define the `storage`, also in the `CartContext.jsx`:

```jsx
//Local Storage
const storage = localStorage.getItem("cartItems")
  ? JSON.parse(localStorage.getItem("cartItems"))
  : [];
```

Finally, in the `CartReducer.jsx`, we will define `Storage`:

```jsx
const Storage = (cartItems) => {
  localStorage.setItem(
    "cartItems",
    JSON.stringify(cartItems.length > 0 ? cartItems : [])
  );
};
```

And export the function to calculate the total price of the cart and the total quantity of the cart

```jsx
export const sumItems = (cartItems) => {
  Storage(cartItems);
  let itemCount = cartItems.reduce(
    (total, product) => total + product.quantity,
    0
  );
  let total = cartItems
    .reduce((total, product) => total + product.price * product.quantity, 0)
    .toFixed(2);
  return { itemCount, total };
};
```

With this, we've successfully completed the implementation of the Shopping App.

Check out the [live demo](https://shop-context.netlify.app/) and the [code repository](https://github.com/israelmitolu/Learn-Context-by-Building-a-Shopping-Website) on Github.

## Conclusion

And we're done!

In the course of this article, we discussed Context and its use and used React to scaffold the interface of a minimal shopping app. We also used context to move data and methods between multiple components and added its functionality using `useReducer` and `dispatch`.

If you found this post useful (and I'm sure you did), do well to share this resource with your friends and co-workers, and follow me for more content. If you have a question or find an error or typo, kindly leave your feedback in the comments section.

Thanks for reading, and happy coding!