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.👇🏻
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.
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:
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:
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.
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.
First, let's create a React app using Vite.js. Just copy the following commands to set up the project.
npm create vite@latest
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.
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>
);
};
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.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.fetchUserData
function accepts id
and uses that ID to fetch the user data. Then we store the response in the user
state.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.
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:
CountContext
object using createContext.
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.
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?
There is also one more library that is also a popular option for state management. The React Recoil.
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.
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