Skip to main content

Overview

Learn how to manage complex state across your application using React’s Context API combined with the useReducer hook. This section covers solutions to prop drilling and scalable state management patterns.

Context API

Share state across component tree without prop drilling

useReducer Hook

Manage complex state logic with actions and reducers

Context Provider

Centralize state management logic

Custom Hooks

Create reusable context consumers

The Problem: Prop Drilling

When state needs to be shared across multiple nested components, passing props through intermediate components becomes cumbersome.
Prop Drilling Issues:
  • Intermediate components receive props they don’t use
  • Changes require updating multiple component signatures
  • Code becomes harder to maintain and refactor

Creating Context

Context provides a way to pass data through the component tree without manually passing props at every level.
1

Create Context

Define the context shape and default values
import { createContext } from 'react';

export const CartContext = createContext({
  items: [],
  addItemToCart: () => {},
  updateItemQuantity: () => {},
});
2

Provide Context

Wrap components that need access to the context
<CartContext.Provider value={ctxValue}>
  {children}
</CartContext.Provider>
3

Consume Context

Access context values in child components
import { useContext } from 'react';
import { CartContext } from '../store/shopping-cart-context';

const { items, updateItemQuantity } = useContext(CartContext);

useReducer Hook

For complex state logic, useReducer offers a more predictable alternative to useState.
  • Multiple state values that change together
  • Complex state updates based on previous state
  • State transitions that follow specific rules
  • When you want to separate state logic from component logic
A reducer takes the current state and an action, returning the new state:
function reducer(state, action) {
  // Return new state based on action
  return newState;
}

Basic Syntax

const [state, dispatch] = useReducer(reducerFunction, initialState);

Complete Example: Shopping Cart

Here’s a real implementation from the course combining Context API with useReducer:
import { createContext, useReducer } from 'react';
import { DUMMY_PRODUCTS } from '../dummy-products.js';

export const CartContext = createContext({
  items: [],
  addItemToCart: () => {},
  updateItemQuantity: () => {},
});

function shoppingCartReducer(state, action) {
  if (action.type === 'ADD_ITEM') {
    const updatedItems = [...state.items];

    const existingCartItemIndex = updatedItems.findIndex(
      (cartItem) => cartItem.id === action.payload
    );
    const existingCartItem = updatedItems[existingCartItemIndex];

    if (existingCartItem) {
      const updatedItem = {
        ...existingCartItem,
        quantity: existingCartItem.quantity + 1,
      };
      updatedItems[existingCartItemIndex] = updatedItem;
    } else {
      const product = DUMMY_PRODUCTS.find(
        (product) => product.id === action.payload
      );
      updatedItems.push({
        id: action.payload,
        name: product.title,
        price: product.price,
        quantity: 1,
      });
    }

    return {
      ...state,
      items: updatedItems,
    };
  }

  if (action.type === 'UPDATE_ITEM') {
    const updatedItems = [...state.items];
    const updatedItemIndex = updatedItems.findIndex(
      (item) => item.id === action.payload.productId
    );

    const updatedItem = {
      ...updatedItems[updatedItemIndex],
    };

    updatedItem.quantity += action.payload.amount;

    if (updatedItem.quantity <= 0) {
      updatedItems.splice(updatedItemIndex, 1);
    } else {
      updatedItems[updatedItemIndex] = updatedItem;
    }

    return {
      ...state,
      items: updatedItems,
    };
  }
  return state;
}

export default function CartContextProvider({ children }) {
  const [shoppingCartState, shoppingCartDispatch] = useReducer(
    shoppingCartReducer,
    {
      items: [],
    }
  );

  function handleAddItemToCart(id) {
    shoppingCartDispatch({
      type: 'ADD_ITEM',
      payload: id,
    });
  }

  function handleUpdateCartItemQuantity(productId, amount) {
    shoppingCartDispatch({
      type: 'UPDATE_ITEM',
      payload: {
        productId,
        amount
      }
    });
  }

  const ctxValue = {
    items: shoppingCartState.items,
    addItemToCart: handleAddItemToCart,
    updateItemQuantity: handleUpdateCartItemQuantity,
  };

  return (
    <CartContext.Provider value={ctxValue}>{children}</CartContext.Provider>
  );
}

Key Concepts

Actions are objects that describe what happened:
dispatch({
  type: 'ADD_ITEM',
  payload: itemId
});

dispatch({
  type: 'UPDATE_ITEM',
  payload: { productId, amount }
});
Action Structure:
  • type: String identifier for the action
  • payload: Data needed to perform the action

Best Practices

Outsource Context Logic

Create separate context provider components to keep logic organized

Type-Safe Actions

Use constants for action types to prevent typos

Granular Contexts

Split unrelated state into separate contexts

Performance

Use multiple contexts to prevent unnecessary re-renders
When to Use Context + useReducer vs Redux:Use Context + useReducer for:
  • App-specific state management
  • Simpler applications
  • When you don’t need middleware
Consider Redux for:
  • Very large applications
  • Complex async logic
  • Time-travel debugging needs

Common Patterns

Action Creators

Wrap dispatch calls in functions for cleaner component code:
function handleAddItemToCart(id) {
  shoppingCartDispatch({
    type: 'ADD_ITEM',
    payload: id,
  });
}

Context Value Object

Combine state and functions into a single context value:
const ctxValue = {
  items: shoppingCartState.items,
  addItemToCart: handleAddItemToCart,
  updateItemQuantity: handleUpdateCartItemQuantity,
};

Resources

React Docs: useReducer

Official documentation for the useReducer hook

React Docs: Context

Learn about React Context in depth

Build docs developers (and LLMs) love