Hello! Welcome back.
In our last lesson, you learned how to use the useReducer hook to manage complex, interconnected state within a single component. We saw how it separates state logic into a reducer function, making your components cleaner and more predictable—a concept that likely feels familiar from your experience with separating concerns in Laravel.
However, useReducer by itself doesn't solve a different, very common problem in React: how do you share state between components that are far apart in the component tree? For example, how does an "Add to Cart" button on a product page update a cart icon in a completely separate header component? Passing the cart state down as props through every intermediate component is inefficient and hard to maintain. This is often called "prop drilling."
Today, we'll solve that problem. You will learn how to create a React Context to provide global state to a component tree. This is the foundation for managing application-wide state, like user authentication or a shopping cart, and is a crucial step toward building our e-commerce project.
1. What is React Context?
React Context is a mechanism designed to solve the problem of prop drilling. It allows you to create a central "store" of data that any component within a certain part of your application can access directly, without needing props passed down to it.
Conceptually, this is similar to how a service container works in Laravel. You "register" a service (or in our case, state) in a central place, and then you can "resolve" (or access) it from anywhere in your application that needs it.
The process of using Context involves three main steps:
- Create a Context object.
- Provide the Context by wrapping a part of your component tree with a special Provider component.
- Consume the Context in any child component that needs the data.
Today, we will focus on the first two steps: creating and providing.
React Context tutorial: Complete guide with practical examples
To get a high-level overview, please read the introductory section of this article from LogRocket. It clearly outlines these three steps.
Read the sections titled "What is React Context?" and "How to use React Context". Focus on understanding the core purpose of Context and the three key verbs: Create, Provide, Use (or Consume).
2. Step 1: Creating the Context
The first step is to create a Context object using React's createContext function. By convention, you'll often do this in a dedicated file.
Let's imagine we're building the shopping cart for our e-commerce app.
- Create a new folder in your
srcdirectory calledcontext. - Inside
src/context, create a file namedCartContext.js.
In this new file, you create the context:
import { createContext } from 'react';
// Create the context
const CartContext = createContext();
export default CartContext;
That's it. createContext() returns a context object that contains two components: a Provider and a Consumer. We'll focus on the Provider.
3. Step 2: Creating a Provider Component
While you could use CartContext.Provider directly in your App.js file, a much more powerful and common pattern is to create a custom wrapper component that manages the state and provides it. This custom component will contain all the state, logic, and functions related to our context.
This provider component will:
- Hold the state (using
useStateor, as we'll see later,useReducer). - Define functions to update that state (e.g.,
addToCart,removeFromCart). - Wrap its
childrenwith the actual<CartContext.Provider>component, passing the state and functions down through thevalueprop.
The following video is a fantastic, practical guide to this process. The presenter refactors a blog application, but the pattern is exactly what we need for any global state.
State Management in React | Context API useContext | React Tutorials for Beginners
Watch this section from a tutorial by Dave Gray to see how to create a custom DataProvider component. This is the core of today's lesson.
Watch from 01:02 to 03:46. Pay close attention to how he: Imports createContext. Defines a DataProvider function that accepts children as a prop. Returns a <DataContext.Provider> component. Renders the {children} inside the provider. Prepares a value prop where the shared data will go.
Following this pattern, our CartContext.js file would look something like this:
import { createContext, useState } from 'react';
// 1. Create the context
const CartContext = createContext();
// 2. Create the Provider component
export const CartProvider = ({ children }) => {
// State for the cart will live here
const [cartItems, setCartItems] = useState([]);
// Functions to modify the cart will live here
const addToCart = (item) => {
// Logic to add item to cartItems
};
return (
// 3. Provide the state and functions to children
<CartContext.Provider value={{ cartItems, addToCart }}>
{children}
</CartContext.Provider>
);
};
export default CartContext;
Everything passed into the value prop will be made available to any component that consumes this context.
4. Step 3: Providing the Context to the App
Now that we have our CartProvider, we need to wrap it around the components that will need access to the cart. For a shopping cart, this is typically the entire application. You do this in your main App.js (or main.jsx/index.js) file.
State Management in React | Context API useContext | React Tutorials for Beginners
Next, watch how the DataProvider is imported and used to wrap the application's component tree.
Watch from 03:46 to 05:03. Notice how he wraps the <Switch> containing all the app's routes with the <DataProvider>. This makes the context available to every page in the application.
In App.js, it would look like this:
import { CartProvider } from './context/CartContext';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import ProductPage from './pages/ProductPage';
import CartPage from './pages/CartPage';
function App() {
return (
<Router>
{/* Wrap the entire app in the CartProvider */}
<CartProvider>
<Header />
<Routes>
<Route path="/" element={<ProductPage />} />
<Route path="/cart" element={<CartPage />} />
</Routes>
</CartProvider>
</Router>
);
}
Now, the Header, ProductPage, CartPage, and any of their children can access the cartItems and addToCart function without any prop drilling.
5. Best Practice: What Belongs in Context?
It can be tempting to put all your application's state into a context, but this is an anti-pattern. Global state should be reserved for data that is genuinely global. Putting local state (like the text in a form input) into a global context can cause performance issues, as components will re-render unnecessarily when unrelated parts of the global state change.
A good rule of thumb:
- Use Context for: State that needs to be accessed and updated by many different components at different levels of the tree.
- Good examples: User authentication status, shopping cart contents, theme (light/dark mode).
- Use local component state (
useState/useReducer) for: State that is only relevant to a single component or its direct children.- Good examples: Whether a modal is open, the value of a form input, data fetched for a specific page.
This final video clip is extremely valuable. The presenter refactors his code to move component-specific logic out of the global context, making his application more efficient and better organized.
State Management in React | Context API useContext | React Tutorials for Beginners
This section demonstrates a crucial refinement. Watch how component-specific state is moved out of the context and back into the components where it's used.
Watch from 21:38 to 26:43. He makes two important changes: He moves the DataProvider to be more specific, wrapping only the components that need post data, leaving the Header and Footer outside. He moves state related to the 'New Post' form (like postTitle and postBody) out of the global context and into the NewPost component itself using useState. This is a critical best practice.
This discipline of separating global and local state is a hallmark of a skilled React developer and aligns with the clean architectural patterns you're used to in backend development.
Conclusion
Today you've learned the fundamental pattern for creating shared, global state in React, effectively eliminating the problem of "prop drilling."
Key Takeaways:
- React Context provides a way to pass data through the component tree without having to pass props down manually at every level.
- The process involves Creating the context with
createContext(), and Providing it with a<Context.Provider value={...}>component. - A common pattern is to create a custom Provider component (e.g.,
CartProvider) that encapsulates the state and logic. - Only truly global state should be placed in a context to maintain performance and code organization.
Preview of the Next Lesson:
We've successfully created and provided our global state. But how do our components actually get that data? In the next lesson, we'll cover the final step: consuming the context using the useContext hook. We will then combine what you learned about useReducer with what you learned today to build a powerful and scalable state management solution for our e-commerce app's shopping cart.