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>
);
}
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
Suppose UserProfile needs the logged-in user's name.
Without Context:
App
|
| passes user
↓
Layout
|
| passes user
↓
Dashboard
|
| passes user
↓
UserProfile
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
React provides direct access:
Context
|
--------------------
| | |
Header Sidebar Profile
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();
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>
);
}
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>
);
}
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
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>
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
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
We can use Context.
const ThemeContext = createContext();
Provider:
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
Consumer:
const theme = useContext(ThemeContext);
Now every component knows the current theme.
Multiple Contexts
Applications often use more than one context.
Example:
App
|
├── AuthContext
|
├── ThemeContext
|
├── LanguageContext
|
└── NotificationContext
Each context has a single responsibility.
This keeps the application organized.
Creating a Custom Hook
Instead of writing:
const user = useContext(UserContext);
everywhere, we can create a reusable hook.
export function useUser() {
return useContext(UserContext);
}
Now components simply use:
const user = useUser();
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
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
containing:
- User
- Theme
- Language
- Notifications
- Shopping Cart
- Settings
- Permissions
Instead, create smaller, focused contexts.
3. Forgetting to Wrap Components with Provider
Incorrect:
<UserProfile />
Correct:
<UserContext.Provider value={user}>
<UserProfile />
</UserContext.Provider>
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
useReducerexists -
useReducervsuseState - Actions and reducers
- Managing complex state
- Combining
useReducerwith 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)