Mastering React Context: Simplifying State Management and Prop Drilling

Mastering React Context: Simplifying State Management and Prop Drilling

Many times, in react, state information is used by multiple components. Information in React is usually shared using props. We use it for this purpose as well.

To resolve such situations, we define that data/function at a common parent component and then move it down using props. We call it lifting the state.

Independent states vs lift up

The Context API is an alternative to this. A context defines a scope with some information. All the components placed in this scope can use that information directly. In my opinion, it is similar to namespaces in C++.

The Problem with Props

Although passing props is a perfectly valid solution, it often creates an issue. There might be situations where the common parent is far from the actual component. So, we will need to move the data very deep down the UI tree and make it unnecessarily verbose. This issue is called "Prop Drilling".

Prop Drilling

Let's take an example to understand this. Let's say we have a CRUD app related to Books. Following is the structure of this application.

Books app using prop drilling

<BookCreate /> - Creates entry for the book. <BookList /> - Shows all the Books <BookShow /> - Display component for each book. <BookEdit /> - Component to editing book information.

The books array is a state variable that contains all the books, and createBook, editBook, and deleteBook are functions that manipulate the state.

As we can see, all the components use the books state. So, we declare in the App component. So are the functions that manipulate it.

Now, these functions need to be moved way down the component tree. We will require a lot of props, resulting in prop drilling.

We can avoid this issue using Contexts. Context lets a parent component provide data to the entire tree below it.

Implementing Context

To implement a Context, you need to do the following three steps:

  1. Creating a context

  2. Provide the context scope

  3. Use the Context

Creating a Context object

We use the createContext() method for this.

import {createContext} from "react";
const Context = createContext(defaultvalue);

This method returns a context object.

Providing the Context

Now, we create a scope using this context object. Here, we define the values to pass to the child components. The context object has a provider component for this purpose.

<Context.Provider value={valueToShare}>
      {children}
</Context.Provider>

This component has a value prop through which we associate values with the scope. The value of this prop becomes available to all the children components of the Provider component.

Using the context object

The children components extract these values using the useContext() hook.

import {useContext} from "react";
const value = useContext(Context);

This hook takes the context object as an argument and returns the values that are associated with it.

Here's an example with a simple implementation using contexts.

//Context.jsx

import { createContext } from "react";

const Context = createContext(1);

export default Context;
//Container.jsx

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

function Container() {
  const value = useContext(Context);
  return <div>{value}</div>;
}

export default Container;
//App.jsx

import React from "react";
import Context from "./Context";
import Container from "./Container";
function App() {
  return (
    <Context.Provider value={5}>
      <Container />
    </Context.Provider>
  );
}

export default App;

Edit context-example

This way, information is shared between components without using any prop for passing data.

Some Extra bits on contexts

  • We can create multiple scopes using the same context object. We can provide each of them with different context values that the components within them can access. Replace the App code with the following code in the above example to see it live.

      function App() {
        return (
          <div>
            <Context.Provider value={3}>
              <Container />
            </Context.Provider>
            <Context.Provider value={7}>
              <Container />
            </Context.Provider>
          </div>
        );
      }
    
  • We can also define a scope within another scope of the same context object, and both will provide different values to their child components. Here, a child can access all the data values belonging to the nearest parent Provider. Replace the App code with the following code in the above example to see it live.

function App() {
  return (
    <div>
      <Context.Provider value={3}>
        <Container />
        <Context.Provider value={7}>
          <Container />
        </Context.Provider>
      </Context.Provider>
    </div>
  );
}
  • If a non-child component tries to use it, it gets the default value passed during the createContext(defaultValue) call. In such a situation, React searches for a parent provider for it. But as it does not find one, it returns the default value as a fallback. Replace the App code with the following code in the above example to see it live.
function App() {
  return (
    <div>
      <Context.Provider value={3}>
        <Container />
      </Context.Provider>
      <Container />
    </div>
  );
}

Practical Use Case

The most common use of contexts is State Management. Prop drilling often happens due to the sharing of state information between components. What would be better is to create a context for the state and put the component tree associated with that state within that.

Let's see our book example again. We declared the books state and associated methods in the App component. We then moved them to the actual location via props.

Alternatively, we can define the books state and the methods in a context and provide it to the App component. This way, the App and all its children components have access to the books state directly.

Books app using context

Here's the code for it

Some Consideration

Contexts are great tools for passing data deeply in a UI tree without tending to prop drilling. They are simple to implement, making it very tempting to use them. Thus, we may end up overusing them. To avoid this, we must understand the association between the information and the components using it.

  • If the information is associated with components which are related and mutually dependent on one another, it might be better to use the prop system as it would make the data flow explicit between them. For example, let's say we have a Form component with multiple form controls. Different form controls may depend upon the states of one another and thus require the sharing of information. Here, using props would be better as it would depict how data flows within the Form component.

  • On the flip side, if the information is associated with mutually independent components, it is better to use contexts. An example of this could be login information. We can use this in the navbar to show the username. We can use this on the profile page to display user details. We can use it application-wide, irrespective of the relationship between the components.

That's all folks

Contexts are great for sharing information when it needs to be moved deeply in a component tree. It is simple to implement and helps in clubbing data in one place.

This article is my understanding of the context API. You may give your insights in the comments. I would appreciate your feedback.

And hey, if you want to connect beyond these pages, catch me on Twitter! My handle is @AnshumanMahato_.

With this, I would like to conclude this post. Until next time, stay curious and keep exploring! ๐ŸŒŸThank You for reading this far. ๐Ÿ˜Š

0 CommentsAdd a Comment