Hello! Welcome back to our course on React.
In the previous lesson, we explored the problem of "prop drilling" and identified it as a major challenge for code maintenance and performance in large applications. We concluded that while passing props is often the right choice, we need a better tool for sharing state that is needed by many distant components.
Today, we'll learn how to use that tool: the React Context API. You will get hands-on experience with the three core steps of implementing context to share data efficiently across your component tree.
By the end of this lesson, you will be able to: create a Context, provide a value with a Provider, and consume it with the useContext hook.
From Prop Drilling to Context
Let's start by revisiting the problem with a concrete example. We have a set of nested components (ComponentA contains B, which contains C, etc.), and we want to pass a user value from the top-level ComponentA all the way down to the deeply nested ComponentD.
React useContext() hook introduction 🧗♂️
This video from Bro Code quickly demonstrates the prop drilling we discussed. It's a perfect practical recap of the problem we're about to solve.
Watch from 03:38 to 06:10. Notice how the user prop has to be passed through ComponentB and ComponentC even though they don't use it.
As you can see, this gets tedious and makes the intermediate components unnecessarily complex. Now, let's solve this using the Context API. The process involves three main steps.
Step 1: Create the Context
First, we need to create a "context object". Think of this as creating a dedicated channel through which our shared data will flow. We do this using the createContext function from React. It's conventional to export this context so other components can use it.
import { createContext } from 'react';
// Create a context for the user data.
// It's exported so other files can import it.
export const UserContext = createContext();
The value you pass to createContext() (in this case, it's undefined because we passed nothing) is the default value. React will use this default value for any component that tries to consume the context but can't find a matching Provider up its component tree.
Step 2: Provide the Context Value
Creating the context doesn't do anything by itself. We now need a "Provider" component to supply a value to this context. The Provider is a special component that you get from your context object (e.g., UserContext.Provider).
You wrap the Provider around a part of your component tree. Any component inside this wrapper can now subscribe to the context. The crucial part is the value prop on the Provider, which is where you pass the data you want to share.

Let's see how to apply steps 1 and 2 to our example.
React useContext() hook introduction 🧗♂️
Continuing with the same video, let's see how to create the UserContext and use its Provider to wrap the child components.
Watch from 06:10 to 08:07. Pay close attention to these three actions: Importing createContext. Creating and exporting the UserContext. Wrapping the child component (ComponentB) with <UserContext.Provider> and passing the user state to the value prop.
Now, ComponentA is a provider. It makes the user state variable available to any component nested inside its UserContext.Provider, no matter how deep.
Step 3: Consume the Context Value
The final step is for the component that needs the data to "consume" it. In modern React, we do this with the useContext hook.
The useContext hook takes your context object as an argument (e.g., useContext(UserContext)) and returns the current value for that context. The value it returns is determined by the value prop of the nearest Provider for that context up in the tree.
Let's watch how ComponentD (and ComponentC) can now consume the user value directly, skipping the intermediate components.
React useContext() hook introduction 🧗♂️
This is where the magic happens. Watch how ComponentD gets the user data without any props being passed from ComponentC.
Watch from 08:07 to 11:08. Focus on how we: Import useContext from React and the UserContext from its file. Call useContext(UserContext) inside ComponentD to get the value. Notice that ComponentB and ComponentC no longer have any prop-related code.
And just like that, we've eliminated prop drilling! The code is cleaner, and ComponentB and ComponentC are no longer cluttered with props they don't need.
You can also review this three-step process in the official React documentation.
Passing Data Deeply with Context
The official React docs provide a concise, step-by-step guide that reinforces what we just saw. Reading this will solidify your understanding.
Read the sections Step 1: Create the context, Step 2: Use the context, and Step 3: Provide the context. This text provides a great summary of the createContext, useContext, and Provider pattern.
Best Practice: Create a Custom Provider Component
In a real application, it's a common and highly recommended practice to create a dedicated component that encapsulates your context's provider and its related state logic. This keeps your code organized and separates state management logic from your UI components.
For instance, instead of putting the useState for the user and the <UserContext.Provider> directly in App.js, you would create a UserProvider component.
All you need to know about the Context API | React Context API Crash Course
PedroTech explains this best practice very clearly. This pattern is key to writing scalable and maintainable React applications with Context.
Watch from 10:28 to 13:25. Focus on the idea of creating a specific UserContextProvider component. Notice how it holds the state (useState) and returns the Provider with the state and functions passed in the value object. This makes your main App component much cleaner.
Here's an example of what that UserProvider might look like:
// In a file like contexts/UserContext.js
import { createContext, useState, useContext } from 'react';
// 1. Create the context
const UserContext = createContext();
// 2. Create the provider component
export function UserProvider({ children }) {
const [user, setUser] = useState({ name: 'Guest', email: '' });
// You can also include functions to update the context value
const login = (userData) => {
setUser(userData);
};
const logout = () => {
setUser({ name: 'Guest', email: '' });
};
const value = { user, login, logout };
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// 3. Create a custom hook for easy consumption
export function useUser() {
return useContext(UserContext);
}
Then, in your main App.js, you'd simply wrap your application with this custom provider:
// In App.js
import { UserProvider } from './contexts/UserContext';
import SomePageComponent from './SomePageComponent';
function App() {
return (
<UserProvider>
<SomePageComponent />
</UserProvider>
);
}
Any component inside UserProvider can now call useUser() to get the user data and the login/logout functions. This is a powerful and clean pattern.
Challenge: Refactor to use Context
Now it's your turn to apply what you've learned. The official React docs have an excellent challenge for this.
Challenge 1 of 1: Replace prop drilling with context
This challenge asks you to refactor a component that uses prop drilling to use Context instead. It's the perfect way to test your understanding.
Read the challenge description under the 'Try out some challenges' heading. Your goal is to remove the imageSize prop that is being drilled down from App -> List -> Place -> PlaceImage. Instead, provide it from App and consume it directly in PlaceImage using Context.
Take some time to think through the steps:
- Create a Context: Where should you create and export a new context for the image size? (A new file like
Context.jsis a good idea). - Provide the Value: In which component should you wrap the children with the
Context.Provider, and what value should you pass? - Consume the Value: In which component should you call
useContextto get theimageSize?
Once you have a plan, try to solve it. The online sandbox in the resource is a great place to experiment. After you've tried, you can look at the solution provided in the resource to compare your approach.
Conclusion
Excellent work! You've successfully learned how to use one of React's most important features for state management. By mastering the Context API, you can now build cleaner, more maintainable applications and avoid the pitfalls of prop drilling.
Key Takeaways:
- The Context API solves prop drilling by creating a "teleporter" for data.
- The process involves three steps:
- Create: Use
createContext()to create a context object. - Provide: Wrap a component tree with
<MyContext.Provider value={...}>to make data available. - Consume: Use the
useContext(MyContext)hook in any descendant component to read the data.
- Create: Use
- A common best practice is to create a custom Provider component that encapsulates both the state logic and the Context Provider itself.
Next Up:
We've used useState to manage the value inside our context provider. This works great for simple values. But what happens when the state logic becomes more complex, involving multiple related state transitions? In our next lesson, we'll explore the useReducer hook as an alternative to useState for managing more complex state, and see how it can be powerfully combined with Context.