paint-brush
How to Simplify State Management With React.js Context API - A Tutorialby@codebucks
New Story

How to Simplify State Management With React.js Context API - A Tutorial

by CodeBucksAugust 2nd, 2024
Read on Terminal Reader

Too Long; Didn't Read

This blog offers a comprehensive guide on managing state in React using the Context API. It explains how to avoid prop drilling, enhance performance, and implement the Context API effectively. With practical examples and optimization tips, it's perfect for developers looking to streamline state management in their React applications.
featured image - How to Simplify State Management With React.js Context API - A Tutorial
CodeBucks HackerNoon profile picture

Hi there👋🏻,


This article is specifically created for beginners who are eager to learn more effective methods for managing state between multiple components. It also aims to address the common issue of prop drilling which can make your code harder to maintain and understand. Let's start with what kind of problem context API solves.


If you prefer the video format, then here is the tutorial that you can watch on my YouTube channel.👇🏻


What Is Prop Drilling?

You know how sometimes you need to pass data from a parent component down to a child component, and you end up passing props through a bunch of components in between? That's called prop drilling, and it can get messy fast. Let’s walk through an example to clarify this.


Props Drilling in React.js

As shown in the diagram, imagine you’ve fetched some data in the App component, which sits at the root of your application. Now, if a deeply nested component, say the Grandchild component, needs to access this data, you’d typically pass it down through the Parent and Child components as props before it reaches Grandchild. This can get ugly as your app grows.


Here is another visual representation:


Reactjs Props Drilling Example

In the example above, the Profile component needs user data, but this data has to travel through the App and Navigation components first, even though these intermediate components don’t use the data themselves. So, how do we clean this up? That’s where the Context API comes in handy.


Props drilling:

  • Increases re-rendering of components
  • Increases boilerplate code
  • Creates component dependency
  • Decreases performance

React Context API

Context API in React.js lets you pass data between components without needing to pass it as props through each level of the component tree. It works like a global state management system where you define your state in a context object, and then you can easily access it anywhere in the component tree. Let's understand this with an example.


React.js Context API

As you can see in the diagram, we have a context object that stores data to be accessed by multiple components. This data is fetched from APIs or third-party services. Before accessing this context data in any component, we need to wrap all the components that require this data in a context provider component.


If we only need to access data in the navigation and profile components, we don't need to wrap up the app component. Once you’ve wrapped the relevant components with the ContextProvider, you can directly access the context data in any component that consumes it. Don't worry if you still don't understand it yet; let's dive into the code and see it in action.


How to Use Context API?

First, let's create a React app using Vite.js. Just copy the following commands to set up the project.


npm create vite@latest


  • Add your project name
  • Select React
  • Select typescript from the options
cd project_name // to change to project directory
npm install
npm run dev


Then you can open your development server http://localhost:5173 in your browser.


First, let's create the required folders. Here is our project's folder structure.

src
  | components
  | context


In the components folder let's create Profile.jsx file, and add the following code.

import React from 'react'

const Profile = () => {
  return (
    <div>Profile</div>
  )
}

export default Profile


Create one more component called Navbar.jsx in the components folder.

import Profile from './Profile'

const Navbar = () => {
  return (
    <nav 
    style={{
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        width: "90%",
        height: "10vh",
        backgroundColor: theme === "light" ? "#fff" : "#1b1b1b",
        color: theme === "light" ? "#1b1b1b" : "#fff",
        border: "1px solid #fff",
        borderRadius: "5px",
        padding: "0 20px",
        marginTop: "40px",
      }}>
        <h1>LOGO</h1>
        <Profile />
    </nav>
  )
}

export default Navbar


Let's import this <Navbar /> component in the App.jsx file.

import Navbar from "./components/Navbar";

function App() {
  return (
    <main
      style={{
        display: "flex",
        flexDirection: "column",
        justifyContent: "start",
        alignItems: "center",
        height: "100vh",
        width: "100vw",
      }}
    >
      <Navbar />
    </main>
  );
}

export default App;


So basically, <Profile /> component is the child of <Navbar /> and <Navbar /> is the child of <App /> component.

Adding Context API

Let's create UserContext.jsx file in the context folder. Add the following code to the file.


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

export const UserContext = createContext();

export const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);

  const fetchUserData = async (id) => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/users/${id}`
    ).then((response) => response.json());
    console.log(response);
    setUser(response);
  };

  useEffect(() => {
    fetchUserData(1);
  }, []);

  return (
    <UserContext.Provider
      value={{
        user,
        fetchUserData
      }}
    >
      {children}
    </UserContext.Provider>
  );
};


  • First, we create an empty UserContext object using createContext. We make sure to import it from react. We can add default values inside the context object, but we keep it null for now.


  • Next, we create UserProvider, which returns a provider using UserContext, like UserContext.Provider. It wraps around the children components, and in the value, we can pass anything we want to use in the child components.


  • Right now, we are using jsonplaceholder API to fetch the user data. The jsonplaceholder provides fake API endpoints for testing purposes. The fetchUserData function accepts id and uses that ID to fetch the user data. Then we store the response in the user state.


  • We are calling fetchUserData function in the useEffect so on page load, it calls the function, and it injects the data in user state.


Now, let's use this context in the <App /> component. Wrap the <NavBar /> component using the <UserProvider />; the same as the following code:

<UserProvider>
  <Navbar />
</UserProvider>


Let's use the user state in the <Profile /> component. For that, we will use useContext hook. That takes UserContext and provides the values that we have passed in the UserProvider such as user state and fetchUserData function. Remember, we don't need to wrap the <Profile /> component since it is already in the <Navbar /> component which is already wrapped with the provider.


Open the Profile.jsx, and add the following code.

  const { user } = useContext(UserContext);

  if (user) {
    return (
      <span
        style={{
          fontWeight: "bold",
        }}
      >
        {user.name}
      </span>
    );
  } else {
    return <span>Login</span>;
  }


Here, we are using user state from the UserContext. We will display the username if there is user otherwise, we will display just a login message. Now, if you see the output there should be a user name in the navbar component. This is how we can directly use any state that is in the context of any components. The component that uses this state should be wrapped within <Provider />.


You can also use multiple contexts as well. You just need to wrap the provider components within another provider component as shown in the following example.

<ThemeProvider>
   <UserProvider>
     <Navbar />
   </UserProvider>
</ThemeProvider>


In the example above, we are using <ThemeProvider /> which manages the theme state.


You can watch the above youtube video to see the full example of using multiple context providers.

Optimizing Re-render in React Context API

There is one problem that occurs when you use the Context API in multiple components. Whenever the state or value changes in the Context API, it re-renders all the components subscribed to that particular context, even if not all the components are using the changed state. To understand this re-rendering issue, let's create a <Counter /> component that uses context to store and display count values.


Check out the following example. You can create a Counter.jsx file in the components folder and paste the following code.


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

const CountContext = createContext();

const CountProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  return (
    <CountContext.Provider value={{ count, setCount }}>
      {children}
    </CountContext.Provider>
  );
};

function CountTitle() {
  console.log("This is Count Title component");
  return <h1>Counter Title</h1>;
}

function CountDisplay() {
  console.log("This is CountDisplay component");
  const { count } = useContext(CountContext);
  return <div>Count: {count}</div>;
}

function CounterButton() {
  console.log("This is CounterButton component");
  const { count, setCount } = useContext(CountContext);
  return (
    <>
      <CountTitle />
      <CountDisplay />
      <button onClick={() => setCount(count + 1)}>Increase</button>
    </>
  );
}

export default function Counter() {
  return (
    <CountProvider>
      <CounterButton />
    </CountProvider>
  );
}


In the above code:

  • First, we create one CountContext object using createContext.


  • In the CountProvider, we have one state to store count values. We are sending count and the setCount method to the child components through value prop.


  • We have created components separately to see how many times individual components re-render.

    • <CountTitle />: This component displays only the title and does not even use any values from the context.

    • <CountDisplay />: This component displays count values and uses count state from the context.

    • <CounterButton />: This component renders both the above component and a button that increases the count values using setCount.


  • At the end, we are wrapping the <CounterButton /> component within the CountProvider component so that the other components can access the count values.


Now, if you run the code and click the Increase button, you'll see in the logs that every component is re-rendering each time the state changes. The <CountTitle /> is not even using count values yet it is re-rendering. This is happening because the parent component of <CountTitle /> which is <CounterButton /> is using and updating the value of count and that's why is re-rendering.


How can we optimize this behavior? The answer is memo. The React memo lets you skip re-rendering a component when its props are unchanged. After the <CountTitle /> component, let's add the following line.


const MemoizedCountTitle = React.memo(CountTitle)


Now, in the <CounterButton /> component, where we are rendering the <CountTitle /> component, replace the <CountTitle /> with <MemoizedCountTitle /> as in the following code:


<>
  <MemoizedCountTitle />
  <CountDisplay />
  <button onClick={() => setCount(count + 1)}>Increase</button>
</>


Now, if you increase the count and check the logs, you should be able to see that it is not rendering the <CountTitle /> component anymore.

Redux vs Context API

The Redux is a state management library for complex state management with more predictable state transitions. While the Context API is designed for simple state management and passing data through the component tree without prop drilling. So, when to choose which?


  • Use React Context API for simple, localized state management where the state is not frequently changing.


  • Use Redux for complex state management needs, especially in larger applications where the benefits of its structured state management outweigh the extra setup.


There is also one more library that is also a popular option for state management. The React Recoil.


  • The React Recoil is a state management library for React that aims to provide the simplicity of Context API with the power and performance of Redux.


If you're interested in learning more about React Recoil, let me know in the comments and I'll create in-depth tutorials on this topic based on your feedback.

Conclusion

The React.js Context API offers a powerful and efficient way to manage states across multiple components, effectively addressing the issue of prop drilling. By using the Context API, you can simplify your code, reduce unnecessary re-renders, and improve overall application performance.


While the Context API is ideal for simple state management, more complex applications may benefit from using Redux or other state management libraries like React Recoil. Understanding when and how to use these tools will enable you to build more maintainable and scalable React applications.


Thanks for reading this article, I hope you found it helpful. If you are interested in learning and building projects using React, Redux, and Next.js, you can visit my YouTube channel here: CodeBucks


Here are my other articles that you might like to read:

Visit my personal blog: DevDreaming