DEV Community

Cover image for React Mastery Series – Day 16: State Management in React – Understanding Context API
Siva Samanthapudi
Siva Samanthapudi

Posted on

React Mastery Series – Day 16: State Management in React – Understanding Context API

Welcome back to the React Mastery Series!

In the previous article, we explored React.memo and useCallback, and learned how they help optimize React applications by reducing unnecessary re-renders.

As applications grow, another challenge begins to appear—not performance, but state sharing.

Imagine building a banking application where the authenticated user information is needed by:

  • Header
  • Sidebar
  • Dashboard
  • Profile page
  • Settings page

Passing the same data through every intermediate component quickly becomes difficult to maintain.

React solves this problem using the Context API.


Why Do We Need Global State?

Every React component can have its own local state.

Example:

function Counter() {

  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );

}
Enter fullscreen mode Exit fullscreen mode

Local state works well when only one component needs the data.

But what happens when multiple components need the same information?

For example:

  • Logged-in user
  • Theme (Light/Dark)
  • Language
  • Shopping cart
  • Application settings

Passing these values manually through multiple levels becomes tedious.


Understanding Prop Drilling

Consider the following component hierarchy:

App
 |
 ├── Layout
 |      |
 |      ├── Sidebar
 |      |
 |      └── Dashboard
 |              |
 |              └── UserProfile
Enter fullscreen mode Exit fullscreen mode

Suppose UserProfile needs the logged-in user's name.

Without Context:

App
 |
 | passes user
 ↓
Layout
 |
 | passes user
 ↓
Dashboard
 |
 | passes user
 ↓
UserProfile
Enter fullscreen mode Exit fullscreen mode

Even though Layout and Dashboard don't use the user, they still have to pass it down.

This is called:

Prop Drilling


Problems with Prop Drilling

As applications grow:

  • Components become tightly coupled.
  • Intermediate components receive unnecessary props.
  • Refactoring becomes harder.
  • Code readability decreases.

Imagine passing:

  • User information
  • Theme
  • Permissions
  • Language
  • Notifications

through six or seven levels of components.

Managing this quickly becomes frustrating.


Introducing Context API

The Context API allows components to share data without manually passing props through every level.

Instead of:

App
 |
 ↓
Layout
 |
 ↓
Dashboard
 |
 ↓
Profile
Enter fullscreen mode Exit fullscreen mode

React provides direct access:

             Context
                |
      --------------------
      |        |         |
   Header   Sidebar   Profile
Enter fullscreen mode Exit fullscreen mode

Any component inside the provider can access the shared data.


Creating a Context

The first step is creating a context.

import { createContext } from "react";

const UserContext = createContext();
Enter fullscreen mode Exit fullscreen mode

createContext() creates an object that can be shared across components.


Creating a Provider

The Provider makes the data available to child components.

function App() {

  const user = {
    name: "Siva",
    role: "Frontend Developer"
  };

  return (
    <UserContext.Provider value={user}>
      <Dashboard />
    </UserContext.Provider>
  );

}
Enter fullscreen mode Exit fullscreen mode

Everything inside UserContext.Provider can access the user object.


Consuming Context with useContext

To access the shared data:

import { useContext } from "react";

function UserProfile() {

  const user = useContext(UserContext);

  return (
    <h2>
      {user.name}
    </h2>
  );

}
Enter fullscreen mode Exit fullscreen mode

No props required.

React directly provides the value from the nearest Provider.


Complete Data Flow

User Logs In
      |
      ↓
Authentication API
      |
      ↓
Store User in Context
      |
      ↓
Context Provider
      |
      ├── Header
      ├── Sidebar
      ├── Dashboard
      └── Profile
Enter fullscreen mode Exit fullscreen mode

Every component receives the same user information.


Updating Context Values

Context isn't limited to read-only data.

We can also share state and functions.

Example:

const [user, setUser] = useState(null);
<UserContext.Provider value={{ user, setUser }}>
  <App />
</UserContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Now every component can:

  • Read user information
  • Update user information

Real-World Example: Authentication

Authentication is one of the most common uses of Context.

Login
   |
   ↓
Authentication API
   |
   ↓
Store User in Context
   |
   ├── Header
   ├── Dashboard
   ├── Settings
   └── Profile
Enter fullscreen mode Exit fullscreen mode

Every screen automatically knows who is logged in.


Theme Management Example

Many applications support Dark Mode.

Instead of passing theme everywhere:

App
 |
 ↓
Navbar
 |
 ↓
Content
 |
 ↓
Card
Enter fullscreen mode Exit fullscreen mode

We can use Context.

const ThemeContext = createContext();
Enter fullscreen mode Exit fullscreen mode

Provider:

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Consumer:

const theme = useContext(ThemeContext);
Enter fullscreen mode Exit fullscreen mode

Now every component knows the current theme.


Multiple Contexts

Applications often use more than one context.

Example:

App
 |
 ├── AuthContext
 |
 ├── ThemeContext
 |
 ├── LanguageContext
 |
 └── NotificationContext
Enter fullscreen mode Exit fullscreen mode

Each context has a single responsibility.

This keeps the application organized.


Creating a Custom Hook

Instead of writing:

const user = useContext(UserContext);
Enter fullscreen mode Exit fullscreen mode

everywhere, we can create a reusable hook.

export function useUser() {
  return useContext(UserContext);
}
Enter fullscreen mode Exit fullscreen mode

Now components simply use:

const user = useUser();
Enter fullscreen mode Exit fullscreen mode

This improves readability and keeps context logic centralized.


When Should You Use Context?

Context is a good choice for:

  • Authentication
  • Theme
  • Language
  • User preferences
  • Feature flags

These values are shared across many components.


When Should You Avoid Context?

Avoid storing everything inside Context.

For example:

  • Search input values
  • Modal visibility for a single page
  • Local form state
  • Component-specific data

Keep local state local.

Overusing Context can cause unnecessary re-renders.


Context vs Props

Props Context
Pass data from parent to child Share data globally
Best for a few component levels Best for many component levels
Explicit data flow Centralized shared data
Simple and predictable Better for application-wide state

Context vs State Management Libraries

Context works well for many applications.

However, as applications become larger, developers often use dedicated state management libraries.

Popular options include:

  • Redux Toolkit
  • Zustand
  • Recoil
  • Jotai

These libraries provide additional features such as:

  • Advanced debugging
  • Middleware
  • Better scalability
  • Fine-grained performance optimizations

We'll explore Redux later in this series.


Enterprise Example: Retail Banking Application

Imagine an online banking portal.

After login:

Login Successful
       |
       ↓
Store User in Context
       |
       ├── Header shows customer name
       ├── Sidebar shows permissions
       ├── Dashboard loads accounts
       ├── Profile displays user details
       └── Settings updates preferences
Enter fullscreen mode Exit fullscreen mode

Without Context, the user object would need to be passed through many intermediate components.

With Context, every component can access it directly.


Common Mistakes

1. Putting Everything in Context

Avoid storing every piece of state globally.

Only share data that multiple components genuinely need.


2. Creating One Huge Context

Avoid:

AppContext
Enter fullscreen mode Exit fullscreen mode

containing:

  • User
  • Theme
  • Language
  • Notifications
  • Shopping Cart
  • Settings
  • Permissions

Instead, create smaller, focused contexts.


3. Forgetting to Wrap Components with Provider

Incorrect:

<UserProfile />
Enter fullscreen mode Exit fullscreen mode

Correct:

<UserContext.Provider value={user}>
  <UserProfile />
</UserContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Without the Provider, consumers won't receive the expected data.


Best Practices

  • Keep Context focused on shared state.
  • Split unrelated concerns into separate contexts.
  • Combine Context with custom hooks.
  • Keep local state inside components whenever possible.
  • Avoid unnecessary updates to Context values.
  • Use dedicated state management libraries for highly complex applications.

Key Takeaways

Today, we learned:

✅ Context API helps share data across components.
✅ It eliminates prop drilling.
createContext, Provider, and useContext are the building blocks.
✅ Context is ideal for authentication, themes, and application-wide settings.
✅ Use Context thoughtfully—don't replace all local state with global state.


Coming Next 🚀

In Day 17, we will explore:

Advanced State Management – Reducers with useReducer

We will learn:

  • Why useReducer exists
  • useReducer vs useState
  • Actions and reducers
  • Managing complex state
  • Combining useReducer with Context API
  • Real-world examples from enterprise applications

useReducer is a powerful tool for managing predictable state transitions and forms the foundation of libraries like Redux.

Happy Coding! 🚀

Top comments (0)