TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

Turn AI service development and operations into one improvement loop

Aidevops.kr covers LLMOps, RAG, agents, observability, evaluation, and cost-performance optimization for production AI services.

Managing Global State with the React Context API

· Updated Apr 16
Managing Global State with the React Context API diagram
Visual guide to the key flow, architecture, and decision points covered in this post.
The React Context API is not just a way to avoid props drilling. It is a mechanism for distributing shared state across the tree, which means it can simplify application structure when used well and cause broad rerenders and mixed responsibilities when used poorly.

State that fits Context and state that does not

Because Context is often described as “global state,” it can look like the answer to every state problem. In practice, its fit is much narrower.

Good fits:

  • theme, locale, and user session information
  • configuration values referenced throughout the screen tree
  • data that changes infrequently but has many consumers

Poor fits:

  • frequently changing form input state
  • fetched lists and cached server data
  • large-scale state with complex update rules

In short, Context is strong for widely shared values, but heavy as a high-frequency state store.

Providers should stay small and clear

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

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}

A good Context usually has one job. Splitting by concerns like AuthContext, ThemeContext, and LocaleContext makes it easier to limit change impact.

The biggest cost of Context is rerendering

Whenever the provider value changes, consumers subscribed to that context rerender. That is the main reason Context becomes heavy.

The following strategies are useful in practice.

  • split one giant context into several small ones
  • separate frequently changing values from mostly static values
  • keep the provider value shape simple instead of recreating new objects constantly
  • consider tools like Zustand or Jotai for state that changes very often

Auth Context is common, but its boundary matters

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  async function login(email, password) {
    const res = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    const data = await res.json();
    setUser(data.user);
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

The boundary gets blurry when an auth provider tries to own everything at once: authorization checks, token refresh, session restore, route guards, and profile fetching. It is usually better to divide those responsibilities across Context, the API layer, and routing logic.

Server state is usually better with a dedicated tool

A common mistake is placing fetched API lists into Context and using it like a cache. Server state brings expiration, retry, synchronization, and refetching concerns with it, so a dedicated tool like TanStack Query is usually a better fit.

  • Context: values shared inside the app
  • Query tools: values fetched from the server and kept in sync

Once that boundary is clear, frontend state design gets much simpler.

Selection criteria

SituationBetter choice
Theme, language, user sessionContext API
Async server dataTanStack Query
Frequently changing client stateZustand / Jotai
Large-scale global state with strong rulesRedux Toolkit

Common mistakes

  • putting every kind of state into a single AppContext
  • using Context as a cache for server data
  • recreating provider objects every render and increasing child rerenders
  • scattering raw useContext calls without custom hooks
  • mixing state, actions, authorization policy, and network requests into one context

Wrap-up

The React Context API is very effective for sharing small, common state. But once it starts acting like a general-purpose store for all global state, both design and performance become heavy very quickly. In production, it is much more stable to keep Context focused on values that are widely shared but do not change often, while leaving server state and more complex state problems to other tools.

What Gets Hard in Production

  • Context looks lightweight, but broad providers can trigger unnecessary renders and hide ownership when they carry frequently changing state.
  • Teams overuse context for convenience and end up with state that is globally reachable but locally hard to reason about.
  • Provider order and initialization logic become a source of bugs when contexts depend on each other implicitly.

Architecture Decisions That Matter

  • Use context mainly for stable cross-cutting values such as theme, auth session shape, feature flags, or dependency injection.
  • Keep rapidly changing server-derived data in dedicated server-state tools instead of pushing it through context.
  • Prefer multiple focused providers over one giant app context.

Practical Example

A good context usually exposes a stable interface rather than raw mutable internals:

type AuthContextValue = {
  userId: string | null
  isAuthenticated: boolean
  signOut: () => Promise<void>
}

const AuthContext = createContext<AuthContextValue | null>(null)

Anti-Patterns to Avoid

  • Putting frequently updated lists or dashboards into a global context.
  • Treating context as a substitute for architectural boundaries.
  • Exporting nullable context without a safe access hook.

Operational Checklist

  • Measure unnecessary re-render hotspots around providers.
  • Keep provider trees intentional and documented.
  • Stabilize context values and callbacks where needed.
  • Review whether a context should really be server cache, route state, or local state.

Final Judgment

Context is excellent for stable shared dependency and configuration flow. It is a poor default store for fast-changing application state.

Continue Reading

Related posts

Next Path

Keep exploring this topic as a system