React Mastery / Advanced Concepts

Context + Reducer - State Management

Combining useReducer with the Context API is the standard way to handle complex global state in React without external libraries. It mimics the Redux pattern (Actions -> Reducer → Store) while staying entirely within the React ecosystem.

🧠 Why Combine Them?

  • Context → global access

  • Reducer → predictable updates

  • Clean separation of logic

Perfect for:

  • Auth

  • Theme

  • Cart

  • User preferences


🧠 The Architecture

In this pattern, the state logic is separated from the UI:

  1. Reducer: A pure function that determines how the state changes based on an action.

  2. Context: A "tunnel" that allows state and dispatch functions to bypass "prop drilling."

  3. Provider: A component that wraps your app and holds the useReducer hook.


🧐 What Are We Building?

In this step, we’ll create a global authentication state using:

  • useReducer → to control how auth state changes

  • Context API → to share auth state across the app

This pattern is commonly used for:

  • Login / Logout

  • User session handling

  • Role-based access


✅ Step-by-Step Implementation

👉 Step 1: Create the Auth Context

Before sharing any global state, we need a Context object.

This context will later hold:

  • The auth state

  • The dispatch function

Think of this as a container that other components can subscribe to.

📁 AuthContext.js

import { createContext } from 'react';

export const AuthContext = createContext();

📍At this point:

  • We are not providing any value

  • We are just creating the context reference


👉 Step 2: Create the Reducer and Initial State

The reducer defines:

  • What the auth state looks like

  • How the state changes in response to actions

Keeping reducer logic in a separate file:

  • Improves readability

  • Makes the logic reusable and testable

📁 authReducer.js

export const initialAuthState = {
  user: null,
  isAuthenticated: false,
};

export function authReducer(state, action) {
  switch (action.type) {
    case 'LOGIN':
      return {
        user: action.payload,
        isAuthenticated: true,
      };

    case 'LOGOUT':
      return initialAuthState;

    default:
      return state;
  }
}

👉 Step 3: Create the Context Provider (Core Step)

This is the heart of the implementation.

Here we:

  1. Initialize useReducer

  2. Store state and dispatch

  3. Pass them through Context.Provider

  4. Wrap the app so all children can access auth data

📁 AuthProvider.jsx

import { useReducer } from 'react';
import { AuthContext } from './AuthContext';
import { authReducer, initialAuthState } from './authReducer';

export default function AuthProvider({ children }) {
  const [state, dispatch] = useReducer(authReducer, initialAuthState);

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

📌 What’s happening here?

  • useReducer controls how state updates

  • AuthContext.Provider controls who can access it

  • children ensures all nested components get access


👉 Step 4: Wrap the App with the Provider

Context only works for components inside the Provider tree.

Wrapping App ensures:

  • Every screen can access auth state

  • No prop drilling needed

📁 App.jsx

import AuthProvider from './AuthProvider';
import Dashboard from './Dashboard';

export default function App() {
  return (
    <AuthProvider>
      <Dashboard />
    </AuthProvider>
  );
}

👉 Step 5: Consume State & Dispatch in Components

Now we use what we built:

  • Read auth state

  • Trigger login/logout using dispatch

This is where business logic meets UI.

📁 Dashboard.jsx

import { useContext } from 'react';
import { AuthContext } from './AuthContext';

export default function Dashboard() {
  const { state, dispatch } = useContext(AuthContext);

  return (
    <>
      {state.isAuthenticated ? (
        <>
          <h2>Welcome, {state.user.name}</h2>
          <button onClick={() => dispatch({ type: 'LOGOUT' })}>
            Logout
          </button>
        </>
      ) : (
        <button
          onClick={() =>
            dispatch({
              type: 'LOGIN',
              payload: { name: 'Bhaskar' },
            })
          }
        >
          Login
        </button>
      )}
    </>
  );
}

📌 What this proves:

  • Any component can access global state

  • UI reacts automatically to state changes

  • Actions drive predictable updates


🧠 Final Mental Model


🧐 When to Use vs. Zustand/Redux

Feature

Context + useReducer

Zustand / Redux

Setup

Built-in to React (No extra bytes)

Requires library installation

Performance

Can cause "bulk" re-renders if not split

Highly optimized selective re-renders

Persistence

Manual (must use useEffect)

Built-in middleware

Complexity

Best for mid-sized apps

Best for massive, complex states

Key Differences

  • Boilerplate: Context requires creating the Provider and wrapping the app; Zustand is a simple hook.

  • State Location: Context state lives in the React Component Tree; Zustand lives in an external store outside the React lifecycle.