Paramclasses Academy - Blog

Tutorials Details

←
Post 17 of 29
→

What is Managing Global State with the React Context API ?

What is Managing Global State with the React Context API ?

October 29, 2026

1. Introduction

In React applications, components often need to share data with other components. For example, an application may need to share the logged-in user's information, the selected theme, or the shopping cart data across multiple components.

Passing data through props works well when components are closely related. However, when data needs to pass through many intermediate components, the code can become difficult to maintain. This problem is known as prop drilling.

The React Context API provides a way to share data across a component tree without manually passing props through every intermediate component.

In this tutorial, we will learn how to create a Context, provide shared values, access them using the useContext Hook, and build practical examples for theme and user management.

2. Learning Objectives

By the end of this tutorial, you will be able to:

  • Understand prop drilling and why it becomes difficult to manage.

  • Explain the purpose of the React Context API.

  • Create a Context using createContext().

  • Provide shared values to components.

  • Read Context values using useContext().

  • Update shared state using React Hooks.

  • Build a theme-switching application.

  • Share user information across multiple components.

  • Understand when Context is appropriate and when another state-management solution may be better.

3. Core Concepts and Explanation

3.1 What Is Prop Drilling?

Prop drilling occurs when data is passed through intermediate components that do not actually need the data themselves.

Consider this component structure:

App
 └── Dashboard
      └── Sidebar
           └── UserProfile

Suppose App contains the user's name, but only UserProfile needs to display it.

Without Context, the name must be passed through Dashboard and Sidebar as props, even though those components do not use it.

Example:

function App() {
  const userName = "Rahul";

  return <Dashboard userName={userName} />;
}

function Dashboard({ userName }) {
  return <Sidebar userName={userName} />;
}

function Sidebar({ userName }) {
  return <UserProfile userName={userName} />;
}

function UserProfile({ userName }) {
  return <h2>Welcome, {userName}!</h2>;
}

This works, but the intermediate components must forward the same prop.

3.2 What Is the React Context API?

The Context API allows a component to make a value available to components deeper in its tree without explicitly passing that value through every intermediate component.

Common use cases include:

  • Current user information.

  • Application themes, such as light and dark mode.

  • Language and localization preferences.

  • Shared application settings.

  • Shopping cart data for a small or medium application.

Context does not automatically manage all application state. It provides a mechanism for sharing values. You can combine Context with useState or useReducer to manage and update shared state.

3.3 Understanding createContext()

The createContext() function creates a Context object.

import { createContext } from "react";

const UserContext = createContext(null);

export default UserContext;

Here, UserContext represents the shared data channel.

The null argument is the default value used when a consumer has no matching provider above it in the component tree.

3.4 Understanding the Provider

A provider makes a Context value available to its descendant components.

The widely compatible syntax is:

<UserContext.Provider value={user}>
  <Dashboard />
</UserContext.Provider>

The value prop contains the data that descendant components can access.

In React 19, you can also use the Context object directly as a provider:

<UserContext value={user}>
  <Dashboard />
</UserContext>

For compatibility with earlier React versions, this tutorial will primarily use .Provider.

3.5 Understanding useContext()

The useContext() Hook reads the nearest matching provider's value.

import { useContext } from "react";
import UserContext from "./UserContext";

function UserProfile() {
  const user = useContext(UserContext);

  return <h2>Welcome, {user.name}!</h2>;
}

If the component is rendered inside a provider containing a user object, it can access that object without receiving it through props.

Important: Calling useContext() does not create shared state by itself. It reads the value provided by the nearest matching provider.

4. Practical Code Examples

4.1 Project Setup

If you need a new React project, create one with Vite:

npm create vite@latest context-demo -- --template react
cd context-demo
npm install
npm run dev

Open the project in VS Code. The following examples can be added to its src directory.

4.2 Example One: Sharing User Information

Create the following files:

src/
 ├── App.jsx
 ├── UserContext.jsx
 ├── UserProfile.jsx
 └── main.jsx

Step 1: Create UserContext.jsx

import { createContext } from "react";

const UserContext = createContext(null);

export default UserContext;

Step 2: Create UserProfile.jsx

import { useContext } from "react";
import UserContext from "./UserContext";

function UserProfile() {
  const user = useContext(UserContext);

  if (!user) {
    return <p>No user information available.</p>;
  }

  return (
    <div>
      <h2>User Profile</h2>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
    </div>
  );
}

export default UserProfile;

Step 3: Update App.jsx

import UserContext from "./UserContext";
import UserProfile from "./UserProfile";

function App() {
  const user = {
    name: "Rahul Sharma",
    email: "rahul@example.com",
  };

  return (
    <UserContext.Provider value={user}>
      <main>
        <h1>My React Application</h1>
        <UserProfile />
      </main>
    </UserContext.Provider>
  );
}

export default App;

How it works:

  1. createContext(null) creates the Context.

  2. App prepares a user object.

  3. UserContext.Provider makes that object available to its descendants.

  4. UserProfile reads the value with useContext(UserContext).

  5. The profile displays the user's name and email.

The intermediate components no longer need to forward the user object as props.

4.3 Example Two: Creating a Dark and Light Theme Switcher

Now let's build a practical example in which a button changes the application's theme.

Update App.jsx:

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

const ThemeContext = createContext(null);

function ThemeButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button onClick={toggleTheme}>
      Switch to {theme === "light" ? "Dark" : "Light"} Mode
    </button>
  );
}

function ThemeContent() {
  const { theme } = useContext(ThemeContext);

  const styles = {
    minHeight: "200px",
    padding: "24px",
    backgroundColor: theme === "light" ? "#ffffff" : "#222222",
    color: theme === "light" ? "#222222" : "#ffffff",
    borderRadius: "12px",
  };

  return (
    <section style={styles}>
      <h2>{theme === "light" ? "Light Theme" : "Dark Theme"}</h2>
      <p>This component reads the shared theme from Context.</p>
      <ThemeButton />
    </section>
  );
}

export default function App() {
  const [theme, setTheme] = useState("light");

  function toggleTheme() {
    setTheme((currentTheme) =>
      currentTheme === "light" ? "dark" : "light"
    );
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <main style={{ padding: "24px" }}>
        <h1>React Context API</h1>
        <ThemeContent />
      </main>
    </ThemeContext.Provider>
  );
}

Expected behavior:

  • Initially, the application displays the light theme.

  • Clicking the button changes the theme to dark.

  • Clicking it again returns to light mode.

  • ThemeContent and ThemeButton both read the shared Context value.

  • Updating the state in App causes the relevant Context consumers to receive the new value.

This example combines useState for state management with Context for sharing the state and its update function.

4.4 Example Three: Creating a Reusable Context Provider

As an application grows, keeping all Context logic inside App.jsx can make the code difficult to maintain. A dedicated provider component can organize it more cleanly.

Create ThemeContext.jsx:

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

const ThemeContext = createContext(null);

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

  function toggleTheme() {
    setTheme((currentTheme) =>
      currentTheme === "light" ? "dark" : "light"
    );
  }

  const value = { theme, toggleTheme };

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

export function useTheme() {
  const context = useContext(ThemeContext);

  if (context === null) {
    throw new Error(
      "useTheme must be used inside a ThemeProvider"
    );
  }

  return context;
}

You can now wrap the application in ThemeProvider.

Update App.jsx:

import { ThemeProvider, useTheme } from "./ThemeContext";

function Dashboard() {
  const { theme, toggleTheme } = useTheme();

  return (
    <div>
      <h2>Current Theme: {theme}</h2>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

export default function App() {
  return (
    <ThemeProvider>
      <h1>My Dashboard</h1>
      <Dashboard />
    </ThemeProvider>
  );
}

The custom useTheme() Hook centralizes Context access and gives a clearer error when used outside the provider.

4.5 How Context Updates Work

When a provider's value changes, React updates the components that consume that Context, subject to React's rendering behavior.

Consider this value:

<ThemeContext.Provider value={{ theme, toggleTheme }}>
  {children}
</ThemeContext.Provider>

Creating a new object for value on each render can cause Context consumers to receive a new value even when its individual properties have not changed.

For larger applications, you can consider:

  • Keeping state as close as practical to the components that need it.

  • Splitting unrelated data into separate Contexts.

  • Using useMemo or useCallback where they meaningfully reduce unnecessary updates.

  • Avoiding overly broad providers that combine unrelated application data.

Memoization should be based on actual needs and measured behavior, not added everywhere automatically.

5. Real-World Applications

5.1 Authentication and User Information

An application can share the current user's basic information across its navigation bar, profile page, and dashboard.

const AuthContext = createContext(null);

The provider might expose the current user and authentication-related actions. Actual authentication and authorization must still be enforced securely by the server; Context alone is not a security boundary.

5.2 E-commerce Shopping Cart

A shopping application can use Context to share cart items and cart-related actions across product cards, the header, and the cart page.

For a large application with complex cart rules, frequent updates, or extensive server synchronization, a more specialized state-management design may be appropriate.

5.3 Language Preferences

Context can share a language preference so that multiple components can display the appropriate translated interface.

5.4 Application Theme

A theme provider can make the selected light or dark theme available across the header, dashboard, settings page, and other components.

6. Common Mistakes and Best Practices

Mistake 1: Forgetting the Provider

If no matching provider exists above a consumer, useContext() returns the Context's default value.

Best practice: Use a clear default value and, when appropriate, a custom Hook that throws a helpful error if the provider is missing.

Mistake 2: Using Context for Every Value

Not all state needs to be global.

Best practice: Keep temporary form values, local dropdown state, and component-specific UI state near the components that use them.

Mistake 3: Creating One Huge Context

Combining theme, authentication, cart, notifications, and unrelated settings into one Context can cause unnecessary coupling and updates.

Best practice: Split unrelated concerns into separate Contexts when that improves clarity and rendering behavior.

Mistake 4: Mutating State Directly

Avoid modifying a shared state object or array directly.

Incorrect:

user.name = "Amit";

Instead, create a new object through the appropriate state setter:

setUser((currentUser) => ({
  ...currentUser,
  name: "Amit",
}));

Mistake 5: Treating Context as a Security Mechanism

Hiding a button or sharing an isAdmin value through Context does not secure sensitive data or protected API endpoints.

Best practice: Validate permissions and access on the server.

Mistake 6: Assuming Context Replaces Every State-Management Tool

Context is excellent for sharing values through a component tree, but it is not automatically the best solution for every complex state-management requirement.

Best practice: Evaluate application size, update frequency, debugging requirements, and data-flow complexity before choosing a state-management architecture.

7. Practice Exercises

Exercise 1 — Basic Context

Create a NameContext and display a user's name in a deeply nested component without passing the name through intermediate props.

Exercise 2 — Theme Switcher

Create a light/dark theme switcher using createContext(), useContext(), and useState().

Exercise 3 — User Profile

Share a user's name, email, and role through a UserContext provider.

Exercise 4 — Custom Hook

Create a useUser() custom Hook that reads from UserContext and displays a helpful error if used outside its provider.

Exercise 5 — Shopping Cart

Create a simple cart Context with:

  • An array of cart items.

  • An action to add an item.

  • An action to remove an item.

  • A component that displays the current item count.

Exercise 6 — Architecture Challenge

Create separate Theme and User Contexts. Explain why keeping unrelated state in separate Contexts may be easier to maintain than using one large Context.

8. Key Takeaways and Conclusion

In this tutorial, we learned that:

  • Prop drilling means forwarding props through intermediate components.

  • createContext() creates a Context object.

  • A provider makes a value available to descendant components.

  • useContext() reads the nearest matching provider's value.

  • Combining Context with useState or useReducer enables shared state management.

  • Custom Hooks can make Context access cleaner and easier to reuse.

  • Context should be applied thoughtfully, with attention to performance, maintainability, and security boundaries.

The React Context API is an important tool for building applications where multiple components need access to shared data. Understanding when to use Context—and when to keep state local—is an essential part of professional React development.

←
Post 17 of 29
→