DEV Community

Cover image for Simplify State Management with React.js Context API Tutorial
CodeBucks
CodeBucks

Posted on

Simplify State Management with React.js Context API Tutorial

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 does 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 given 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 above example, 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 dependancy
  • 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 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 setup the project.

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode
  • Add your project name
  • Select React
  • Select typescript from options
cd project_name // to change to project directory
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

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

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

src
  | components
  | context
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create one more component called Navbar.jsx in 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

Adding Context API

Let's create UserContext.jsx file in the context folder. Add the following code in 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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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 use that id to fetch the user data. Then we are storing 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 /> same as the following code:

<UserProvider>
  <Navbar />
</UserProvider>
Enter fullscreen mode Exit fullscreen mode

Let's use the user state in <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 <Profile /> component since it is already in the <Navbar /> component which is already wrapped with 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>;
  }
Enter fullscreen mode Exit fullscreen mode

Here, we are using user state from the UserContext. We will display 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 in any components. The component that uses this state should be wrapped within <Provider />.

You can also use multiple context 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>
Enter fullscreen mode Exit fullscreen mode

In the above example 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 is not even using any values from the context.
    • <CountDisplay />: This component displays count values and is using 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)
Enter fullscreen mode Exit fullscreen mode

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>
</>
Enter fullscreen mode Exit fullscreen mode

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 these topic based on your feedback.

Conclusion

The React.js Context API offers a powerful and efficient way to manage state 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 project using React, Redux and Next.js you can visit my YouTube channel here: CodeBucks

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

Visit my personal blog website from here: DevDreaming

Top comments (0)