Scientyfic World

React Context API for Global State: Reducer Pattern and Re-render Fixes

The React Context API lets you share a value, such as the current user, theme or shopping cart, with every component below a provider, without passing props through each level....

Share:

Get an AI summary of this article

Blog banner for React Context API for global state management

The React Context API lets you share a value, such as the current user, theme or shopping cart, with every component below a provider, without passing props through each level. For global state management, combine it with useState or useReducer in a provider component, expose the state and the update functions through context, and read them with use() or useContext() wherever they’re needed.

This guide builds a small shopping cart with Context and useReducer in React 19, shows the re-render problem most Context setups have (and how splitting contexts fixed it in my test), and explains when Context is enough and when a library such as Zustand or Redux Toolkit is the better tool. Every example was run in a browser.

The Problem: Prop Drilling

Without Context, data that many components need has to be passed down through every component in between, even those that don’t use it:

<App user={user}>
  <Layout user={user}>
    <Header user={user}>
      <Avatar user={user} />      {/* the only component that needs it */}

This “prop drilling” is fine for one or two levels (see passing data between React components), but it gets painful as the tree grows. Context lets Avatar read user directly from the nearest provider above it.

Context Basics in React 19

import { createContext, use, useState } from 'react';

// 1. Create the context (the argument is the default when there's no provider)
const ThemeContext = createContext('light');

// 2. Provide a value
function App() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext value={theme}>
      <Page />
    </ThemeContext>
  );
}

// 3. Read it anywhere below
function Button() {
  const theme = use(ThemeContext);     // or useContext(ThemeContext)
  return <button className={theme}>Save</button>;
}

Two React 19 changes appear here: you render <ThemeContext value={...}> directly (in React 18 it was <ThemeContext.Provider>, which still works), and use() can read context, even inside conditions, unlike useContext(). The old <Context.Consumer> render-prop API and the legacy contextTypes API from class components aren’t needed in new code; the legacy API was removed in React 19.

Global State with Context and useReducer

For state that many components read and update, put the state in a provider component and manage it with a reducer, so every change goes through one function with named actions. This cart provider uses two contexts: one for the cart data and one for the dispatch function:

import { createContext, use, useMemo, useReducer } from 'react';

const CartContext = createContext(null);
const CartDispatchContext = createContext(null);

function cartReducer(items, action) {
  switch (action.type) {
    case 'added': {
      const existing = items.find((i) => i.id === action.product.id);
      if (existing) {
        return items.map((i) => (i.id === action.product.id ? { ...i, qty: i.qty + 1 } : i));
      }
      return [...items, { ...action.product, qty: 1 }];
    }
    case 'removed':
      return items.filter((i) => i.id !== action.id);
    case 'cleared':
      return [];
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

export function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);
  const value = useMemo(() => {
    const count = items.reduce((sum, i) => sum + i.qty, 0);
    const total = items.reduce((sum, i) => sum + i.qty * i.price, 0);
    return { items, count, total };
  }, [items]);

  return (
    <CartContext value={value}>
      <CartDispatchContext value={dispatch}>{children}</CartDispatchContext>
    </CartContext>
  );
}

export function useCart() {
  const cart = use(CartContext);
  if (!cart) throw new Error('useCart must be used inside <CartProvider>');
  return cart;
}

export function useCartDispatch() {
  const dispatch = use(CartDispatchContext);
  if (!dispatch) throw new Error('useCartDispatch must be used inside <CartProvider>');
  return dispatch;
}

Components then use the custom hooks and never touch the contexts directly:

import { memo } from 'react';
import { CartProvider, useCart, useCartDispatch } from './CartContext';

const products = [
  { id: 'kb', name: 'Keyboard', price: 49 },
  { id: 'ms', name: 'Mouse', price: 19 },
];


function CartBadge() {
  const { count } = useCart();
  return <span id="badge">Cart ({count})</span>;
}

// Only needs dispatch, so it does not re-render when the cart changes
const ProductCard = memo(function ProductCard({ product }) {
  const dispatch = useCartDispatch();
  return (
    <li>
      {product.name} ${product.price}{' '}
      <button onClick={() => dispatch({ type: 'added', product })}>Add {product.name}</button>
    </li>
  );
});

function CartSummary() {
  const { items, total } = useCart();
  const dispatch = useCartDispatch();
  return (
    <aside id="summary">
      {items.map((i) => (
        <p key={i.id}>
          {i.name} × {i.qty}{' '}
          <button onClick={() => dispatch({ type: 'removed', id: i.id })}>Remove {i.name}</button>
        </p>
      ))}
      <strong>Total: ${total}</strong>
    </aside>
  );
}

export default function Shop() {
  return (
    <CartProvider>
      <header><CartBadge /></header>
      <ul>{products.map((p) => <ProductCard key={p.id} product={p} />)}</ul>
      <CartSummary />
    </CartProvider>
  );
}

In my test, adding the keyboard twice and the mouse once showed “Cart (3)”, “Keyboard × 2”, “Mouse × 1” and “Total: $117”; removing the keyboard brought the badge back to “Cart (1)”. The custom hooks throw a clear error if a component is used outside the provider, which is easier to debug than a null value.

The Re-render Problem (and How Splitting Context Fixes It)

Every component that reads a context re-renders whenever the context value changes, and memo can’t stop it. The most common setup puts state and functions into one object:

// The common mistake: one context, a new object on every render
import { createContext, memo, use, useState } from 'react';

const AppContext = createContext(null);

function AppProvider({ children }) {
  const [count, setCount] = useState(0);
  return <AppContext value={{ count, add: () => setCount((c) => c + 1) }}>{children}</AppContext>;
}

const ProductCard = memo(function ProductCard({ name }) {
  const { add } = use(AppContext);       // only uses add, but re-renders on every count change
  return <li><button onClick={add}>Add {name}</button></li>;
});

function Badge() {
  const { count } = use(AppContext);
  return <span id="badge">Cart ({count})</span>;
}

export default function SingleShop() {
  return (
    <AppProvider>
      <Badge />
      <ul><ProductCard name="Keyboard" /><ProductCard name="Mouse" /></ul>
    </AppProvider>
  );
}

Here the provider creates a new object on every render, so every consumer re-renders after every change, including ProductCard, which only needs add. I counted renders in a production build:

SetupProductCard renders on loadAfter 3 cart updates
One context with { count, add }28 (both cards re-rendered every time)
Separate state and dispatch contexts (cart example)22 (no re-renders)

In the split version, only CartBadge and CartSummary, which read the cart, re-rendered (from 1 to 5 renders each after four updates). Techniques that help:

  • Split contexts by how often they change: state vs dispatch, or user vs theme vs cart.
  • Memoize the value with useMemo when it’s an object, so it only changes when its contents do.
  • Pass dispatch or stable functions (from useReducer, or wrapped in useCallback) instead of recreating functions in the value.
  • Keep context close to where it’s used; not everything has to live at the app root.

The React Compiler can memoize the value object for you, but it doesn’t change the rule that every consumer re-renders when the value changes, so splitting still matters for fast-changing state.

When to Use Context, and When to Use a Library

NeedGood fit
Values that rarely change: theme, locale, signed-in user, feature flagsContext
Small to medium app state shared by a few areas (cart, wizard, filters)Context + useReducer
Frequently changing state read by many components; components should re-render only for the slice they useZustand (selectors) or Jotai (atoms)
Large app, many developers, strict patterns, time-travel debuggingRedux Toolkit (see global state with React Redux)
Data from an API: caching, refetching, loading statesTanStack Query (server state isn’t global client state; see fetching data with React hooks)
State that belongs in the URL: filters, tabs, paginationURL search params

Context is a way to pass a value, not a state manager by itself; the state still lives in useState or useReducer. It works well for global state up to the point where re-renders become a problem, and libraries with selectors exist precisely to solve that. A dark mode toggle is a typical good use, as in my guide to adding dark mode in React.

Best Practices

  • Wrap each context in a provider component and custom hooks (useCart, useCartDispatch) so components don’t import contexts directly.
  • Throw a helpful error in the hook when there’s no provider.
  • Use useReducer once the state has several related actions; it keeps update logic testable and in one place.
  • Split state and dispatch, and split unrelated data into separate contexts.
  • Don’t put server data (API responses) in Context if you need caching and refetching; use a data-fetching library.
  • Avoid one giant “AppContext” holding everything.

Conclusion

The React Context API removes prop drilling by letting any component read a value from the nearest provider. For global state, keep the state in a provider with useReducer, expose it through separate state and dispatch contexts with small custom hooks, and memoize object values. In my test, that split took unnecessary re-renders from six to zero for components that only dispatch actions. When many components read fast-changing state, switch to Zustand, Jotai or Redux Toolkit, which can re-render only the components that use the changed slice.

FAQs

What is the React Context API used for?

Sharing values such as the current user, theme, locale or a shopping cart with many components without passing props through every level. Combined with useState or useReducer, it can manage global state.

Is React Context a replacement for Redux?

For small and medium apps, often yes. Context plus useReducer covers shared state well. Redux Toolkit or Zustand are better when many components read frequently changing state or the app needs strict patterns and dev tooling.

Why do all my context consumers re-render?

Every component that reads a context re-renders when its value changes, and a new object created on each render counts as a change. Memoize the value, split state and dispatch into separate contexts, and split unrelated data.

What is the difference between use and useContext?

Both read a context value. use() was added in React 19 and can be called inside conditions and loops; useContext() follows the normal rules of hooks.

How do I use Context with useReducer?

Call useReducer in a provider component, put the state in one context and dispatch in another, and expose them through custom hooks. Components dispatch actions such as { type: “added”, product }.

Should I use the Provider component in React 19?

You can render the context directly, for example <ThemeContext value={theme}>. <ThemeContext.Provider> still works.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts