A Beginner guide to global state management in React using context API.

Introduction

I bet you are reading this article because you want to understand how React context API works but also you finally realize that it is really hard to manage states between two or more React components because sometimes we often pass states through components that will never use that state.

Props Drilling

So initially this is called props drilling, Props drilling is a process whereby a nested component needs a state variable that can not be directly passed to it because it exists several levels down, so to pass a state variable to that nested component, we have to pass that variable through other components to get to that specific nested component.

Let’s use a real-life scenario as an example to fully understand props drilling, so let's say you are in a classroom, sitting at the end of the class, and you need to pass a book to your friend who is sitting at the other end, you can do that by passing the book from one student to another until it gets to your friend, even though these students don’t need the book, it still passes through them because they are the only direct means of transporting the book to your friend. In React we call this props drilling.

And it has its disadvantages, one of which is if the book you are trying to pass to your friend contains vital information that only you and your friends should know, other students might be able to have this information.

And sometimes it is slow to send the book over to your friend through that process.

Raising state variables to the parent component

There is another method of passing a state variable between children components, and it is referred to as Raising a state to the parent component, so supposing two components share the same parent, and these components need to share state variables as well, the simple logical thing to do is to bring the state variables to the parent component that they both share so that they can easily have access to it.

But what if these two components do not share the same parent component? That is where the context API comes in.

The Context API

Think of it as a universal parent to all of the children components, and it allows its children to have access to its state variables without any form of props drilling. Even raising a state to a parent component will include some aspects of props drilling to pass states to the children components, but the context API does not work that way.

The context API is a way of managing state variables globally and it allows its children components to have access to these state variables no matter how deeply nested they are, using the useContext hook.

So instead of passing the book from one student to another in the class, you can magically create a portal between you and your friend. But of course, we don’t do magic in programming.

Why you should use the Context API.

First, it is easier to manage the state variables if they are all in one place, and with context API all the shared state variables are stored in the context provider. And secondly, no more props drilling, by now you must have understood how unprofessional props drilling is and why you should avoid it, one of the easiest ways to avoid props drilling is through the context API.

How to use the Context API.

I will provide a tutorial example on how to use the context API, here is a link for the completed version of what we are building.

But to follow along, you need to understand React functional components and how React hooks work.

It is okay if you don’t have a lot of experience with the above prerequisites, you can also follow along, I will be explaining any concept that might sound new to you.

I will just assume you have a react app ready. If you don’t, open your terminal and then type npx create-react-app appname. And once that is done, open the application in your favorite code editor and get ready to code.

Or you can use codesandbox to quickly start a react project right in the browser.

Once everything is ready, replace the App.css file contents with this;

* {
  padding: 0px;
  margin: 0px;
  box-sizing: border-box;
}
.navbar ul {
  padding: 10px;
  list-style-type: none;
  display: flex;
  justify-content: space-around;
  align-items: center;
  background-color: greenyellow;
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}

.numbs {
  font-weight: 800;
}

.products li {
  display: flex;
  align-items: center;
  justify-content: space-around;
  border: 1px solid gray;
  padding: 10px;
}

.products li div {
  width: 20%;
  display: flex;
}
.products li div button {
  width: 50%;
  padding: 10px 20px;
  margin: 2px
}

Create a new directory in the src folder and name it components. Inside the new directory, you created, create 3 JS or JSX files, with the following names navbar.jsx, products.jsx and product.jsx.

In the navbar.jsx file, write this;

import React from "react";

const Navbar = () => {
  return (
    <nav className="navbar">
      <ul>
        <li>
          Items in Cart <span className="numbs">{3}</span>
        </li>
      </ul>
    </nav>
  );
};

export default Navbar;

And then in the App.js, replace the content with this;

import Navbar from "./components/navbar";
import "./App.css";

export default function App() {
  return (
    <div>
      <Navbar />
    </div>
  );
}

On the first line, we import the Navbar component from ./components/navbar and on the next, we import the ./App.css file.

In the App component, we return a JSX expression that includes the Navbar component.

Now if you try to run the development server, by typing npm start on the terminal you are going to see something similar to this;

image.png

In the product.jsx file, write this;

import React from "react";

const Product = ({ name = "Red T-shirt", id = 1 }) => {
  return (
    <>
      <span className="name">{name}</span>
      <div className="actions">
        <button title='add to cart'>+</button>
        <button title='remove from cart'>-</button>
      </div>
    </>
  );
};

export default Product;

And also write this to the products.jsx file.

import React from "react";
import Product from "./product";

const Products = () => {
  return (
    <ul className="products">
      <li><Product /></li>
    </ul>
  );
};

export default Products;

In the products.jsx file we import React from 'react' and on the next line we import the Product component from ./product, and include it in between the li tag.

Now we need to import the Products component from './components/products.jsx into the App.js file, so update the App.js file with this;

import Navbar from "./components/navbar";
import Products from "./components/products";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <Navbar />
      <Products />
    </div>
  );
}

If you preview the result of the changes we made in the browser, you will see something similar to this;

image.png

Let me explain the whole concept of this project and why using `React context API is the only logical way to manage the state variables.

So we have a navbar that contains the number of items we have in a cart, this navbar exists independently as a component and also we have a Products component which shows the list of items we can add to the cart. These items exist as nested components in the Products component.

Now let's set up the context API component, in the src directory create JS or JSX file and name it GlobalContext.

To create a context in the GlobalContext component we need to import the createContext from 'react' and initialize it.

So add this to the GlobalContext file;

import {  createContext } from "react";

const GlobalContext = createContext();

And then create a component that will wrap the child components in the Context provider.

import {  createContext } from "react";

const GlobalContext = createContext();

const GlobalProvider = ({children}) => {


  return <GlobalContext.Provider>
    {children}
  </GlobalContext>
}

The children reference that was passed in between the Context provider is destructured from the props of the GlobalProvider function (component) and it represents any other child component that will be passed between the GlobalProvider component. For example;

//this is an example, you don't need to add it to the code.
<GlobalProvider>
    <h1>Hello World</h1> //This a child component to GlobalProvider 
    <p>Hello, React</p> //also a child component to GlobalProvider
</GlobalProvider>

The context provider needs one more thing, and that is a value props. This represents the data that needs to be shared with the children components through the useContext hook.

So let's go ahead and create a state variable and then add it to the context provider value props.

import {  createContext, useState } from "react";

const GlobalContext = createContext();

const GlobalProvider = ({children}) => {
  const [cart, setCart] = useState([])

  return <GlobalContext.Provider value={{cart}}>
    {children}
  </GlobalContext>
}

And finally, we need to export both GlobalContext and GlobalProvider, so add this to the GlobalContext.jsx file.

export { GlobalContext,  GlobalProvider }

Now in the index.js we need to import the GlobalProvider component and wrap it around the App component, like this.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import App from "./App";
import { GlobalProvider } from "./GlobalContext";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <GlobalProvider>
      <App />
    </GlobalProvider>
  </StrictMode>
);

Now we can be able to access all the data passed to the value props of the GlobalProvider components, in any child component via the useContext hook. Let's try to get the value of the cart and add it to the Navbar component, to show the number of items we have in the cart.

In the navbar.jsx file, we are going to import the GlobalContext we initialized in the GlobalContext.js file, and also import the useContext hook from 'react'.

import React, { useContext } from "react";
import { GlobalContext } from "../GlobalContext";
const Navbar = () => {
  const {cart} = useContext(GlobalContext)
  return (
    <nav className="navbar">
      <ul>
        <li>
          Items in Cart <span className="numbs">{cart.length}</span>
        </li>
      </ul>
    </nav>
  );
};

export default Navbar;

After importing the useContext hook from 'react' and importing GlobalContext from GlobalContext.jsx file.

We passed the GlobalContext we imported as an argument to the useContext hook, and then since it will return an object, we destructured it and get the cart from it. Hence this line:

const {cart} = useContext(GlobalContext)

Since the cart state variable is an array, we can use the length method to get the number of items that are inside the array, so we passed cart.length in between the span tag.

<li>
  Items in Cart <span className="numbs">{cart.length}</span>
</li>

If you try to see the changes on the browser you will see that it says Items in the cart is 0, because currently, there is no item in the cart array.

We are able to get a state variable from a different component to another nested component without any form of props drilling using the context API.

How can we update the cart state variable from another component? Let's say the Product.jsx component.

The thing is you can also pass a function in the value props of the Context provider and here is how to do that.

In the GlobalContext.js, let's create a function and name it addToCart and also include it in the value props of the Context Provider.

import { createContext, useState } from "react";

const GlobalContext = createContext();

const GlobalProvider = ({ children }) => {
  const [cart, setCart] = useState([]);
  const addToCart = (data) => {
    setCart([...cart, data])
  }
  return (
    <GlobalContext.Provider value={{ cart, addToCart }}>{children}</GlobalContext.Provider>
  );
};

export {GlobalContext, GlobalProvider}

Let's try using the addToCart function in the Product component. We have to import the useContext hook from 'react' and the context instance GlobalContext from the GlobalContext.jsx file.

Update the product.jsx file with this.

import React, { useContext } from "react";
import { GlobalContext } from "../GlobalContext";

const Product = ({ name = "Red T-shirt", id = 1 }) => {
  const { addToCart } = useContext(GlobalContext);
  return (
    <>
      <span className="name">{name}</span>
      <div className="actions">
        <button onClick={() => addToCart({name, id})} title="add to cart">+</button>
        <button title="remove from cart">-</button>
      </div>
    </>
  );
};

export default Product;

Now try clicking on the add to cart button, you will see that on the Navbar component the number of items in the cart will start increasing.

image.png

The more you click on the button, the more that single item is added to the cart multiple times, so let's try disabling the add to cart button if an item is already inside the cart but before we do that, let's create other several items.

So in the GlobalProvider component create an array of objects with name and id properties, you can name the array items and also include it in the context provider value props.

import { createContext, useState } from "react";

const GlobalContext = createContext();

 const items = [
    {name: "White Gown", id: 2},
    {name: "Pink Dress", id: 3},
    {name: "Dodgerblue Cap", id: 4},
    {name: "Green Jacket", id: 5},
    {name: "Pink Shoes Jacket", id: 6},

  ]

const GlobalProvider = ({ children }) => {
  const [cart, setCart] = useState([]);
  const addToCart = (data) => {
    setCart([...cart, data])
  }

  return (
    <GlobalContext.Provider value={{ cart, addToCart, items }}>{children}</GlobalContext.Provider>
  );
};

export {GlobalContext, GlobalProvider}

In the Products component from the products.jsx file, import the useContext hook and also the context instance GlobalContext from the GlobalContext.jsx file. Destructure the returned data from the useContext hook, to get the items array.

And then using the map method to render all the items in the array.

import React, { useContext } from "react";
import { GlobalContext } from "../GlobalContext";

import Product from "./product";

const Products = () => {
  const { items } = useContext(GlobalContext);

  return (
    <ul className="products">
      {items.map((item) => {
        return <li  key={item.id}><Product name={item.name} id={item.id} /></li>
      })}
    </ul>
  );
};

export default Products;

If you open the browser to see your changes, it is going to look like this:

image.png

Now let's try disabling the add to cart button of an item if it exists in the cart array.

Update, the product.jsx file with this;

import React, { useContext, useEffect, useState } from "react";
import { GlobalContext } from "../GlobalContext";

const Product = ({ name = "Red T-shirt", id = 1 }) => {
  const { cart, addToCart } = useContext(GlobalContext);
  const [inCart, setInCart] = useState(false)

  useEffect(() => {
    if(cart.filter((crt) => crt.id === id).length === 0){
      setInCart(false)
    } else {
      setInCart(true)
    }
  }, [cart])

  return (
    <>
      <span className="name">{name}</span>
      <div className="actions">
        <button disabled={inCart} onClick={() => addToCart({name, id})} title="add to cart">+</button>
        <button disabled={!inCart} title="remove from cart">-</button>
      </div>
    </>
  );
};

export default Product;

We imported the useEffect hook from react, if you don't fully understand how the useEffect hooks works, you can take a look at this article.

The useEffect hook is a function that accepts two arguments, first a function to execute and second an array of dependencies which are state variables. A function passed to the useEffect hook will only be executed when the state variable dependencies change. In our case, that is when we add or remove an item from the cart.

What we are doing here is filtering the entire cart to find an item that matches the id of the current item, and when we can't find one, we set the inCart state variable to false but when we found one we set it to true.

So the buttons will either be disabled or enabled based on the value of the inCart state variable.

If you check this on the browser, you'll see by default all the remove from cart buttons are disabled but once we click to add an item to the cart, they become enabled while the add to cart button becomes disabled.

image.png

The final thing we need to do now is to make sure we can be able to remove items from the cart by clicking the remove from cart button.

So in the GlobalProvider component create a function called removeFromCart which accepts an argument of id.

import { createContext, useState } from "react";

const GlobalContext = createContext();
const items = [
  {name: "White Gown", id: 2},
  {name: "Pink Dress", id: 3},
  {name: "Dodgerblue Cap", id: 4},
  {name: "Green Jacket", id: 5},
  {name: "Pink Shoes Jacket", id: 6},

]
const GlobalProvider = ({ children }) => {
  const [cart, setCart] = useState([]);
  const addToCart = (data) => {
    setCart([...cart, data])
  }
  const removeFromCart = (id) => {
    setCart(cart.filter((crt) => crt.id !== id))
  }

  return (
    <GlobalContext.Provider value={{ cart, addToCart, items, removeFromCart }}>{children}</GlobalContext.Provider>
  );
};

export {GlobalContext, GlobalProvider}

And now in the Product component, let's get the removeFromCart function and pass it to the onClick event of the remove from cart button.

import React, { useContext, useEffect, useState } from "react";
import { GlobalContext } from "../GlobalContext";

const Product = ({ name = "Red T-shirt", id = 1 }) => {
  const { cart, addToCart, removeFromCart } = useContext(GlobalContext);
  const [inCart, setInCart] = useState(false)

  useEffect(() => {
    if(cart.filter((crt) => crt.id === id).length === 0){
      setInCart(false)
    } else {
      setInCart(true)
    }
  }, [cart])

  return (
    <>
      <span className="name">{name}</span>
      <div className="actions">
        <button disabled={inCart} onClick={() => addToCart({name, id})} title="add to cart">+</button>
        <button disabled={!inCart} onClick={() => removeFromCart(id)} title="remove from cart">-</button>
      </div>
    </>
  );
};

export default Product;

As you can see, we are also passing the id of the item as an argument, to know exactly what item we are going to remove from the cart.

Now we can be able to add and remove items from the cart while showing the number of current items in the cart at the Navbar component.

Did you find this article valuable?

Support Usman Gurowa Hassan by becoming a sponsor. Any amount is appreciated!