Back to Blog
Lesson 11 of the Intermediate React: Hooks, State & Data Patterns course
ReactJune 25, 20263 min read

Architecting Global State with Context and Reducer

Master professional state management by combining Context API and useReducer. Learn how to build a centralized, scalable store for your React dashboard.

reacthooksstate managementcontext apiusereducerarchitecturejavascriptfrontend

Previously in this course, we explored Complex State with useReducer: A React Developer's Guide to handle intricate logic locally, and Introduction to Context API: Avoiding Prop Drilling in React to pass data through the tree. While both are powerful, they are often insufficient on their own for large applications. Today, we combine them to create a robust, centralized architecture for global state management.

The Architecture of Global State

In a growing dashboard, you'll find yourself passing "update" functions down through multiple layers of components. This is the classic prop-drilling problem. By pairing useReducer with the Context API, we create a "store" that provides both the current state and a way to update it (the dispatch function) to any component in the tree.

Think of the useReducer as the "brain" (the logic) and the Context as the "nervous system" (the delivery mechanism).

Building a Centralized Store

To build this, we need three distinct parts:

  1. The Reducer: The pure function that defines how state changes.
  2. The Context: The container for our state and dispatch.
  3. The Provider: The component that wraps our app and injects the state.

Here is how we set this up for our dashboard's global UI settings:

JSX
import React, { createContext, useReducer, useContext } from CE9178">'react';

// 1. Define the initial state and reducer
const initialState = { sidebarOpen: true, theme: CE9178">'dark' };

function dashboardReducer(state, action) {
  switch (action.type) {
    case CE9178">'TOGGLE_SIDEBAR':
      return { ...state, sidebarOpen: !state.sidebarOpen };
    case CE9178">'SET_THEME':
      return { ...state, theme: action.payload };
    default:
      return state;
  }
}

// 2. Create the Context
const DashboardContext = createContext();

// 3. Create the Provider
export const DashboardProvider = ({ children }) => {
  const [state, dispatch] = useReducer(dashboardReducer, initialState);

  return (
    <DashboardContext.Provider value={{ state, dispatch }}>
      {children}
    </DashboardContext.Provider>
  );
};

// 4. Custom hook for easy access
export const useDashboard = () => useContext(DashboardContext);

Implementing the Store in the Dashboard

Now that our store is defined, we wrap our application root (or a specific feature boundary) with the DashboardProvider. Because we exported the useDashboard hook, consuming the state becomes trivial and type-safe.

JSX
// Inside any component in the tree
const SidebarToggle = () => {
  const { state, dispatch } = useDashboard();

  return (
    <button onClick={() => dispatch({ type: CE9178">'TOGGLE_SIDEBAR' })}>
      Sidebar is {state.sidebarOpen ? CE9178">'Open' : CE9178">'Closed'}
    </button>
  );
};

This pattern removes the need to pass functions as props. If you need to add a new piece of state—like userProfile—you simply update the reducer and the initial state object. The rest of your application remains untouched.

Hands-on Exercise

Refactor your existing dashboard layout to use this pattern.

  1. Create a DashboardProvider that tracks a notifications array.
  2. Create an action type ADD_NOTIFICATION that appends a string to that array.
  3. Add a button in your Header component that dispatches this action.
  4. Add a NotificationList component elsewhere in your dashboard that consumes the state to display these items.

Common Pitfalls

  • Over-contextualizing: Don't put everything in one global context. If your state updates frequently (e.g., a timer or a scroll position), it will trigger a re-render of every component consuming that context. We will cover how to mitigate this in a future lesson on performance.
  • Missing Providers: If you try to use useDashboard() outside of the DashboardProvider, it will return undefined. Always ensure your hook handles this or your Provider wraps the components correctly.
  • Reducer Bloat: As your dashboard grows, your reducer might become massive. Remember Managing Object-Based State: Immutable Updates in React and consider splitting your state into smaller, logical contexts if the reducer becomes unmanageable.

Recap

By combining useReducer and Context API, we have successfully decoupled the state transition logic from the view layer. We now have a centralized store that allows any component to trigger state changes via dispatch without knowing how those changes occur. This is the cornerstone of professional React application architecture.

Up next: We will dive into creating a dedicated Theme Context to handle dynamic UI styling globally.

Similar Posts