useContext in react: everything you need to know

useContext in react: everything you need to know

only understand this: Using useContext is not necessary, we use it to avoid prop drilling.

Here's how prop drilling works:
Let's say the grandfather component has a certain state and the great-grandson component needs to use this state. Now, in order to provide the state to great-grandson, grandfather will give it to father which in turn will give state to son, which finally provides it to great-grandson component.
Now, here there are only 2 layers between grandfather and great-grandson component and still it became a cumbersome process to pass property or state to great-grandson component.
Another thing to note is that the 2 layers between grandfather and great-grandson components do not even need this state but we are still passing the state to them.

This is prop drilling ☝️.

Now here comes the hero: useContext.

Using useContext the great-grandchild can directly access the state whenever required without it passing through all the layers.
Hence, when we use the useContext hook, the state or property of the topmost ancestor can be accessed by any child component as required.

We only have to do 3 things to use useContext, just three, no big deal:

  1. First: We need to create a context using createContext.

  2. Second: We need to wrap context around the topmost ancestor using the Provider property.

  3. Third: we'll use the context inside the child components using useContext.

Enough talking, let's get to code.

Our final goal is to make this 👇.

1.gif

Now, you may ask why this ☝️. because our final goal is to understand use Context and not to make a TikTok clone!

We'll play with 5 files: index.js, App.js, cart-context.js, Cart.js, and ProductListing.js

index.js and App.js: You already know about these.
cart-context.js: To store and play with context.
Cart.js: file for the below component👇

2.PNG

ProductListing.js: file for the below component👇

3.PNG

Let's have a look inside all the above files one by one:

//cart-context.js

import { createContext } from "react";

export const CartContext = createContext();

☝️ what I told you the first step is:
First: We need to create a context using createContext.
and this is exactly what we did, we created context by first importing createContext from 'react' then creating the context and naming the context as CartContext.

Ok, let's move on:

//index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";

import { CartContext } from "./cart-context.js";
import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <CartContext.Provider value={{ item: 6 }}>
      <App />
    </CartContext.Provider>
  </StrictMode>,
  rootElement
);

☝️ what I told you the second step is:
Second: We need to wrap context around the topmost ancestor using the Provider property.
I know what question you have in mind: what's this 👇

    <CartContext.Provider value={{ item: 6 }}>
    </CartContext.Provider>

This is nothing fancy, just plain syntax that you have to remember.
basically CartContext is an object which has different properties, out of which we wanna use the Provider property. Hence, the syntax. We made <CartContext.Provider> the topmost ancestor and wrapped our App/ inside it.

Read that again ☝️.

You may again ask: what's value={{ item: 6 }}?
**value is the keyword and part of the syntax and {item: 6} is the object we passed so that whichever component wants can access this object. However, instead of object, we can also pass a state. What I just said is pure gold 😉, read it again. ** Now you may ask, as why to use object and not pass 6 as a normal variable. Here's why: we use objects so that we can pass more values in the future inside the same object. objects are extensible.

let's move on:

//App.js

import {Cart} from './Cart';
import {ProductListing} from './ProductListing';

export default function App() {
  return (
    <div className="App">
      <h1>eCommerce</h1>
      <div>
        <Cart />
        <ProductListing />
      </div>
    </div>
  );
}

In the above code, there are 2 components (Cart and ProductListing) and everything else is self-explanatory.

Let's finally move on to the 2 components that need to share state (Cart and ProductListing). Recall that just a few lines above, we have created a context in cart-context.js. Now is the time to use it.

and this is third step that I told you about: Third: we'll use the context inside the child components using useContext.

//cart.js

import { useContext } from "react";
import { CartContext } from "./cart-context";

export function Cart() {
  const { item } = useContext(CartContext);
  return <h1> Items in cart {item}</h1>;
}

In order to use the context created in cart-context.js, we imported { useContext } from 'react' and { CartContext } from './cart-context' then destructured (If you don't know how destructuring works, read my blog about the same.)the object passed in cart-context.js using useContext(CartContext) and extracted the value of item. Recall that the value of item was 6 in cart-context.js. and we finally get this:

4.PNG

Now, here's the logic: if we can pass the value 6 from cart-context to the Cart component, we can also pass a state.

Let's move on to ProductListing.js

//ProductListing.js
import { useContext } from "react";
import { CartContext } from "./cart-context";

export function ProductListing() {
  return ["1", "2", "3", "4"].map((item) => <div><h2>Product {item}</h2><button>Add To Cart</button></div>);
}

why we imported useContext and CartContext in the above file? because we'll need it later when we pass the state and dynamically update the items in cart after the click happend on Add To Items button.

As of now, here's our App. We cannot update items in cart yet as the state has not been shared.

5.PNG

**Now, that you have understood the basics, let's do some refactoring to make our code look sexy (Is it wrong to say this word). ** (You'll find useContext on the internet mostly in this form):

Why we are doing refactoring: Because why to take ingredients to cook food everywhere. Let's first cook all the food in cart-context.js then distribute.

//cart-context.js

import { createContext, useContext } from "react";

const defaultValue = { item: 4 };

const CartContext = createContext(defaultValue);

const useCart = () => useContext(CartContext);

function CartProvider({children}){
  return(
    <CartContext.Provider value = {{ item: 6 }}>
      {children}
    </CartContext.Provider>
  )
}

export {CartProvider, useCart};

What's all this ☝️: This is the most important file and we consolidated all the context-related work in this file such that you won't find the word context in any other file. Now, let's understand one by one as to what is happening here:

  1. defaultValue: you'll learn about it after the index.js code.

  2. useCart: we used useContext in this so we do not have to import useContext and use useContext in every child component.

  3. CartProvider: Do not think too much, only know that this is just a fancy function. Inside the CartProvider function, we used the syntax that we previously used in index.js so that our index.js looks clean. Let's have a look at index.js to understand more.

//index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { CartProvider } from './cart-context'

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <CartProvider>
      <App />
    </CartProvider>
  </StrictMode>,
  rootElement
);

Let's understand ☝️ First, observe that our index.js is looking much cleaner.

  1. Instead of { CartContext } we imported { CartProvider }, because we no longer need CartContext as the syntax which uses it, is already wrapped inside CartProvider function in cart-context.js. Hence we'll directly import {CartProvider}.

  2. what is CartProvider?: We defined the CartProvider function in cart-context.js. Here we made it the main ancestor inside which our App is wrapped.

  3. what is defaultValue in cart-context.js: It is a fall safe that is in case we forget to wrap App/ inside CartProvider, the defaultValue defined in cart-context.js will overpower and hence, instead of 6, 4 will be printed (Items in Cart: 4).

//cart.js

import { useCart } from "./cart-context";

export function Cart() {
  const { item } = useCart();
  return <h1> Items in cart {item}</h1>;
}

Only 2 changes:

  1. Instead of { useContext }, we imported { useCart } and removed { useContext }.

  2. destructured { item } using useCart().


//ProductListing.js

import { useCart } from "./cart-context";

export function ProductListing() {
  return ["1", "2", "3", "4"].map((item) => (
    <div>
      <h2>Product {item}</h2>
      <button>Add To Cart</button>
    </div>
  ));
}

Not much changed: we imported { useCart } and removed { useContext }.

//App.js

import "./styles.css";

import { Cart } from "./Cart";
import { ProductListing } from "./ProductListing";

export default function App() {
  return (
    <div className="App">
      <h1>eCommerce</h1>
      <div>
        <Cart />
        <ProductListing />
      </div>
    </div>
  );
}

Nothing changed.

In case you forgot, our App looks the same:

6.PNG

Now, This is the final stage, let's pass the state. Only one major change: Instead of passing { item: 6 }, we'll pass state { items, setItems }so that it can be used by all components as required.
See the below code. (I promise this is the final stage of this app).

//cart-context.js

import { createContext, useContext, useState } from "react";

const defaultValue = { item: 6 };

const CartContext = createContext(defaultValue);
const useCart = () => useContext(CartContext);

function CartProvider({ children }) {

  const [items, setItems] = useState(0);

  return (
    <CartContext.Provider value={{ items, setItems }}>{children}</CartContext.Provider>
  );
}

export { CartProvider, useCart };

Only 3 minor modifications:

  1. imported {useState} from 'react'.

  2. declared [items, setItems]

  3. passed { items, setItems } object in value.

//Cart.js

import { useCart } from "./cart-context";

export function Cart() {
  const { items } = useCart();
  return <h1> Items in cart {items}</h1>;
}

Only modification: replaced item with items as items represents the state that we passed.

//ProductListing.js

import { useCart } from "./cart-context";

export function ProductListing() {

  const {setItems} = useCart();

  function addToCart(){
    setItems(items => items+1)
  }

  return ["1", "2", "3", "4"].map((item) => (
    <div>
      <h2>Product {item}</h2>
      <button onClick={addToCart}>Add To Cart</button>
    </div>
  ));
}

only

  1. got the setItems function after destructuring useCart().

  2. added onClick which calls addToCart function.

voilà!!!!!!!! Here's our final App👇

10.gif

Just one more thing I want to show you real quick (Ignore, if you don't wanna wrap your head around it).

11.PNG

See above in devtools that Cart and ProductListing and App, all the children of CartProvider (and Context.Provider). similarly, we create different context for different use cases. This blog is already too long if you wanna know how to create more ancestors tell me in the comments and I'll be happy to add it to this blog.

Finally we saw that both the components, Cart and ProductListing are able to finally access the state defined in cart-context.js

Here's the codesandbox link if you wanna play with code: codesandbox.io/s/for-hashnode-1-3kh1lv

This topic was kind of tricky to explain in a blog, still, I tried my best. It it added some value, tell me in the comments.

That's all folks.

If you have any doubt ask me in the comments section and I'll try to answer as soon as possible.

I write articles related to web development. Follow me here if you are learning the same.

If you love the article follow me on Twitter: @therajatg

If you are the Linkedin type, let's connect: linkedin.com/in/therajatg

Have an awesome day ahead 😀!