Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 58

Scalable State with useReducer and useContext

Recommended resources

Hello! In the last few lessons, we've explored two powerful hooks in isolation: useReducer for managing complex state logic, and useContext for delivering state deep into our component tree. Now, it's time to bring them together.

Today's lesson addresses a central challenge in building large React applications: how do you manage state that is both complex (having many possible actions, like a shopping cart) and global (needed by many different components)? The answer is a powerful pattern that combines useReducer and useContext. This combination gives you the structured state management of a reducer with the app-wide reach of context, forming a robust and scalable solution without needing external libraries like Redux.

This is a cornerstone technique for building the e-commerce application you're aiming for.

1. The "Why": Global State Meets Complex Logic

First, let's clarify why this combination is so necessary.

  • useReducer is excellent for state where the next value depends on the previous one and is updated through multiple, distinct actions (e.g., ADD_TO_CART, REMOVE_FROM_CART, UPDATE_QUANTITY). However, by itself, its state is local to the component where it's defined.
  • useContext is perfect for making data available globally, but using it with useState can become clumsy if you need to pass down many different state-updating functions.

Passing the state and dispatch function from a useReducer hook down through many layers of props would be tedious and error-prone. The elegant solution is to place the state and dispatch function into a context, making them available to any component that needs them.

The following video provides an excellent overview of this exact problem and why combining these two hooks is the ideal solution.

React Hooks Tutorial - 22 - useReducer with useContext

This video from Codevolution sets the stage by explaining the problem of managing a shared state (like a counter) across deeply nested components and introduces the idea of using useContext to provide the state and dispatch from useReducer.

Watch from the beginning to 03:21. The video clearly illustrates the issue of 'prop drilling' and presents the combination of useReducer and useContext as the solution for managing global state.


2. The Core Pattern: Building a Shopping Cart

Now let's dive into the "how." We'll walk through a complete, practical example of building a shopping cart state management system. This is a classic use-case and directly applicable to your e-commerce project goal.

The pattern involves three main parts:

  1. The Reducer: Defines the state shape and the logic for all possible actions.
  2. The Provider: A component that uses the useReducer hook and provides its state and dispatch function to its children via context.
  3. The Consumer: Any component that uses the useContext hook to access the state and dispatch actions.

We'll follow a video that implements this pattern beautifully.

Part 1: Setting Up the Reducer and Context

First, we need to define the structure of our state and the reducer function that will manage it. We'll also create the React Context that will act as the channel for our data.

useContext with useReducer React Hooks Crash Course

Let's set up the foundational pieces: the reducer logic and the context itself. This video walks through building a shopping cart system, which is a perfect real-world example.

Watch from 02:37 to 07:30. Focus on: (02:37 - 04:17): Creating the ShopContext with createContext and defining the initialState (with total and products) in a separate shopReducer.js file. (04:17 - 07:30): Building the shopReducer function. Notice how it uses a switch statement to handle different action types ('ADD_TO_CART', 'REMOVE_FROM_CART', 'UPDATE_PRICE') and always returns a new, immutable state object.

Part 2: Creating the Provider and a Custom Hook

This is where we connect the reducer to the context. We'll create a dedicated ShopProvider component. This component will:

  1. Call useReducer to get the current state and dispatch function.
  2. Wrap its children in the ShopContext.Provider.
  3. Pass the state and functions to update it through the provider's value prop.

A common best practice, shown in the video, is to also create a custom hook (e.g., useShop) that wraps useContext. This makes consuming the context cleaner and more straightforward.

useContext with useReducer React Hooks Crash Course

Now, we'll create the ShopProvider component. This component is the bridge between our reducer logic and the rest of our app. It will manage the state using useReducer and provide it to all its children via the context.

Continue watching from 07:30 to 14:13. Key points are: (07:30 - 12:53): See how useReducer is called within the ShopProvider. Note the creation of helper functions like addToCart that dispatch actions. This is a great pattern for abstracting away the raw dispatch calls. (12:53 - 13:42): The creation of the custom useShop hook. This neatly encapsulates the useContext call. (13:42 - 14:13): Wrapping the entire application in App.js with the <ShopProvider> to make the context available everywhere.

Part 3: Consuming the State and Dispatching Actions

With the provider in place and our custom useShop hook ready, accessing the global cart state from any component becomes simple.

useContext with useReducer React Hooks Crash Course

With the provider in place, we can now easily access our global cart state from any component. Watch how the custom useShop hook makes this incredibly clean and intuitive.

Finally, watch from 14:13 to the end (19:43). Observe how: The ProductCard component calls useShop() to get the addToCart and removeFromCart functions and attaches them to button clicks. The Cart component calls useShop() to get the products array and the total price to display them, demonstrating how components can subscribe to and display the global state.


3. Encapsulation and Scalability

The pattern you just saw—placing the reducer, context, provider, and custom hook logic together—is highly effective. It creates a self-contained "state module" that is easy to maintain and reuse. Your main application components stay clean, focusing on presentation, while the state logic is neatly encapsulated.

The official React documentation reinforces this powerful pattern. Reading it will help solidify your understanding and show you how this structure helps your application scale.

Scaling Up with Reducer and Context

The video showed a complete, practical implementation. The official React documentation reinforces this pattern and explains the benefits of organizing your code this way, which is a great way to solidify your understanding.

Read the sections 'Combining a reducer with context', 'Moving all wiring into a single file', and the final 'Recap'. Pay close attention to how they create a TasksProvider component and custom hooks (useTasks, useTasksDispatch), just like in the video. This modular approach is the key to a scalable React application.

As the documentation notes, you can have many such context-reducer pairs in your app. For your e-commerce project, you might have one for the shopping cart (CartProvider), another for user authentication (AuthProvider), and perhaps another for UI state like open modals (UIProvider).


Conclusion

You've now learned one of the most important state management patterns in modern React. By combining useReducer and useContext, you can build sophisticated, scalable applications without immediately reaching for a large state management library.

Key Takeaways:

  • The Pattern: Place the state and dispatch returned from useReducer into a Context Provider's value.
  • Encapsulation: The best practice is to create a dedicated provider component (e.g., ShopProvider) that contains the useReducer logic and provides the context.
  • Clean Consumption: Create a custom hook (e.g., useShop) to simplify accessing the context's value in consumer components.
  • Scalability: This modular pattern allows you to manage different "slices" of global state independently, keeping your application organized as it grows.

Preview of the Next Lesson:

We've spent a lot of time on state management. In the next lesson, we'll shift gears to a different kind of tool: the useRef hook. You'll learn how to use it to create references to DOM elements, allowing you to directly interact with them—for example, to manage focus, trigger animations, or integrate with third-party DOM libraries.