Context API in React
The Context API is a built-in React mechanism for sharing data between multiple components without having to pass that data manually through every level of the component tree.
It is especially useful for application-wide or shared data such as:
- Logged-in user information
- Authentication state
- Theme — light/dark
- Language/localization
- Application settings
- Shopping cart
- Permissions/roles
- Global UI state
- Configuration values
The important idea is:
Context allows a component to provide data to many descendant components, and those components can consume the data directly without receiving it through props.
Why Do We Need Context API?
Consider a component hierarchy:
App
└── Layout
└── Header
└── UserMenu
└── Profile
Suppose App has the logged-in user:
const user = {
id: 1,
name: "Syed",
email: "syed@example.com"
};
And Profile needs this user.
Without Context, you might have to pass the user through every component:
App
↓ user
Layout
↓ user
Header
↓ user
UserMenu
↓ user
Profile
For example:
<App>
<Layout user={user}>
<Header user={user}>
<UserMenu user={user}>
<Profile user={user} />
</UserMenu>
</Header>
</Layout>
</App>
This is called prop drilling.
The intermediate components may not actually need user. They are only receiving it so they can pass it to another component.
Context solves this problem:
UserContext
│
▼
App
│
Layout
│
Header
│
UserMenu
│
▼
Profile
│
useContext(UserContext)
Now Profile can directly access the user.
What Is Prop Drilling?
Prop drilling means passing data through components that don't need the data themselves.
Example:
function App() {
const user = {
name: "Syed"
};
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />;
}
function Header({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <Profile user={user} />;
}
function Profile({ user }) {
return <h2>{user.name}</h2>;
}
The data travels through:
App
↓
Layout
↓
Header
↓
UserMenu
↓
Profile
But only Profile actually uses it.
As the application grows, this becomes difficult to maintain.
Context changes the architecture to:
UserContext
│
├── Header
├── Sidebar
├── Profile
├── Dashboard
└── UserMenu
Any component underneath the provider can access the context.
Context API Core Concepts
There are three fundamental pieces:
1. createContext()
↓
2. Provider
↓
3. Consumer
In modern React, consumption is normally done with:
useContext()
So the typical flow is:
createContext
↓
Context Provider
↓
Shared Value
↓
useContext
↓
Component
1. Creating a Context
The first step is creating a context.
import { createContext } from "react";
const UserContext = createContext(null);
createContext() creates a context object.
It does not create the actual user state.
It creates a mechanism through which a value can be supplied to descendant components.
You can think of it as defining a channel:
UserContext
│
│ user data
▼
Components
2. Providing Context
Creating a context isn't enough.
Some component must provide a value.
With the traditional provider syntax:
<UserContext.Provider value={user}>
<App />
</UserContext.Provider>
The value becomes available to components underneath the provider.
For example:
function App() {
const user = {
id: 1,
name: "Syed"
};
return (
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
);
}
Now:
UserContext.Provider
│
└── Dashboard
├── Header
├── Sidebar
└── Profile
All these descendants can access the context.
3. Consuming Context
A component can consume the context using useContext().
import { useContext } from "react";
function Profile() {
const user = useContext(UserContext);
return <h2>{user.name}</h2>;
}
The component does not need:
<Profile user={user} />
Instead, it gets the value directly from the context.
Complete Basic Example
Let's build a complete example.
UserContext.tsx
import { createContext } from "react";
export const UserContext = createContext(null);
App.tsx
import { UserContext } from "./UserContext";
import Profile from "./Profile";
function App() {
const user = {
id: 1,
name: "Syed",
email: "syed@example.com"
};
return (
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>
);
}
export default App;
Profile.tsx
import { useContext } from "react";
import { UserContext } from "./UserContext";
function Profile() {
const user = useContext(UserContext);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
export default Profile;
The important architecture is:
App
│
│ UserContext.Provider
│ value = user
│
▼
Profile
│
│ useContext(UserContext)
▼
user
Context API with Nested Components
One of the biggest advantages of Context is that the consuming component does not have to be a direct child.
function App() {
const user = {
name: "Syed"
};
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
Layout might contain several layers:
function Layout() {
return <Header />;
}
function Header() {
return <Navigation />;
}
function Navigation() {
return <Profile />;
}
Profile can still access the context:
function Profile() {
const user = useContext(UserContext);
return <h2>{user.name}</h2>;
}
The structure becomes:
App
│
└── Provider
│
└── Layout
│
└── Header
│
└── Navigation
│
└── Profile
│
└── useContext()
There is no need to pass user through Layout, Header, or Navigation.
Context API Does Not Mean "Global Variable"
This is an important distinction.
Context is often described as providing "global data", but technically it is not a JavaScript global variable.
A context value is available only to components inside the corresponding Provider.
For example:
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
Dashboard can access it.
But this component:
<Profile />
outside the provider cannot access that provider's value.
So think of Context as:
Shared data scoped to a component subtree.
Context API with Default Value
You can provide a default value when creating the context.
const ThemeContext = createContext("light");
Now:
const theme = useContext(ThemeContext);
If the component is not inside a ThemeContext.Provider, React returns:
"light"
For example:
function App() {
return <ThemeButton />;
}
function ThemeButton() {
const theme = useContext(ThemeContext);
return <button>Current theme: {theme}</button>;
}
The output is:
Current theme: light
because no Provider exists.
Context with Provider
Now provide a different value:
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemeButton />
</ThemeContext.Provider>
);
}
Now:
const theme = useContext(ThemeContext);
returns:
dark
The Provider value overrides the default context value.
Conceptually:
createContext("light")
│
│ default
▼
"light"
Provider value="dark"
│
▼
"dark"
Context API with State
This is where Context becomes significantly more useful.
Instead of providing a fixed value:
value="dark"
you can provide React state:
const [theme, setTheme] = useState("light");
Then:
<ThemeContext.Provider
value={{
theme,
setTheme
}}
>
Now every consumer can access both:
- Current state
- Function to modify state
Example:
import { createContext, useState } from "react";
const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Dashboard />
</ThemeContext.Provider>
);
}
A child component can consume it:
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeContext);
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<button onClick={toggleTheme}>
Current Theme: {theme}
</button>
);
}
Now the architecture is:
Context
│
┌────────┴────────┐
│ │
theme setTheme
│ │
▼ ▼
Read current Change state
When setTheme() changes the state:
setTheme()
↓
State changes
↓
Provider re-renders
↓
Context value changes
↓
Consumers re-render
Context + State Is One of the Most Common Patterns
A very common architecture is:
const SomeContext = createContext(null);
function SomeProvider({ children }) {
const [state, setState] = useState(...);
return (
<SomeContext.Provider value={{
state,
setState
}}>
{children}
</SomeContext.Provider>
);
}
Then:
function ComponentA() {
const { state } = useContext(SomeContext);
// read state
}
And:
function ComponentB() {
const { setState } = useContext(SomeContext);
// modify state
}
This effectively creates a shared state mechanism.
Creating a Custom Provider Component
Instead of putting the Provider directly inside App, it is usually cleaner to create a dedicated Provider component.
For example:
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Then:
function App() {
return (
<ThemeProvider>
<Dashboard />
</ThemeProvider>
);
}
This is much cleaner for larger applications.
The architecture becomes:
App
│
└── ThemeProvider
│
├── Header
├── Sidebar
└── Dashboard
All descendants can access the theme.
Why children Is Used in a Provider
Consider:
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
When you write:
<ThemeProvider>
<App />
</ThemeProvider>
the <App /> becomes children.
So effectively:
children = <App />
The Provider places those children inside the Context Provider.
This makes the Provider reusable.
Recommended Pattern: Context + Custom Hook
You can further improve the design by creating a custom hook.
Instead of every component doing:
const { theme, setTheme } = useContext(ThemeContext);
create:
export function useTheme() {
return useContext(ThemeContext);
}
Now components simply use:
const { theme, setTheme } = useTheme();
This is a very common production pattern.
Complete Theme Example
ThemeContext.tsx
import {
createContext,
useContext,
useState
} from "react";
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(current =>
current === "light" ? "dark" : "light"
);
};
return (
<ThemeContext.Provider
value={{
theme,
toggleTheme
}}
>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
Then:
function App() {
return (
<ThemeProvider>
<Dashboard />
</ThemeProvider>
);
}
And anywhere underneath:
function Dashboard() {
const { theme, toggleTheme } = useTheme();
return (
<div>
<h2>Theme: {theme}</h2>
<button onClick={toggleTheme}>
Toggle Theme
</button>
</div>
);
}
The overall functionality is:
ThemeProvider
│
├── theme
│
└── toggleTheme()
│
▼
Dashboard
│
├── Reads theme
│
└── Changes theme
This is one of the most useful Context patterns to learn.
Context API for Authentication
Authentication is another excellent use case.
Suppose your application has:
const user = {
id: 1,
name: "Syed",
role: "Admin"
};
Many components may need this information:
Header
Dashboard
Sidebar
Profile
AdminPanel
UserMenu
Instead of passing user through props, create an authentication context.
const AuthContext = createContext(null);
Provider:
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => {
setUser(userData);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider
value={{
user,
login,
logout
}}
>
{children}
</AuthContext.Provider>
);
}
Then:
function Header() {
const { user, logout } = useContext(AuthContext);
return (
<header>
<span>{user?.name}</span>
<button onClick={logout}>
Logout
</button>
</header>
);
}
Another component can independently access the same authentication state:
function Dashboard() {
const { user } = useContext(AuthContext);
return (
<h2>
Welcome {user?.name}
</h2>
);
}
Both components use the same context state.
AuthProvider
│
┌───────┴────────┐
│ │
Header Dashboard
│ │
└───────┬────────┘
│
Same user
Context API for Shopping Cart
Context is also useful for a shopping cart.
Suppose:
const [cart, setCart] = useState([]);
Many components need access to it:
ProductList
ProductCard
Header
CartIcon
CartPage
Checkout
Without Context:
ProductList
↓ cart
ProductCard
↓ cart
...
With Context:
CartProvider
│
┌─────────┼─────────┐
│ │ │
Header Products CartPage
│ │ │
└─────────┼─────────┘
│
Same cart
Example:
const CartContext = createContext(null);
function CartProvider({ children }) {
const [cart, setCart] = useState([]);
const addToCart = product => {
setCart(current => [
...current,
product
]);
};
const removeFromCart = id => {
setCart(current =>
current.filter(item => item.id !== id)
);
};
return (
<CartContext.Provider
value={{
cart,
addToCart,
removeFromCart
}}
>
{children}
</CartContext.Provider>
);
}
A product component can do:
function ProductCard({ product }) {
const { addToCart } = useContext(CartContext);
return (
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
);
}
And the header can do:
function CartIcon() {
const { cart } = useContext(CartContext);
return (
<span>
Cart ({cart.length})
</span>
);
}
Both are using the same shared state.
Context API for Permissions
Context can also provide authorization information.
For example:
const user = {
name: "Syed",
role: "Admin",
permissions: [
"users.read",
"users.create",
"users.delete"
]
};
You could expose:
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const hasPermission = permission => {
return user?.permissions?.includes(permission);
};
return (
<AuthContext.Provider
value={{
user,
hasPermission
}}
>
{children}
</AuthContext.Provider>
);
}
Then:
function UserManagement() {
const { hasPermission } = useContext(AuthContext);
if (!hasPermission("users.read")) {
return <p>Access denied</p>;
}
return <Users />;
}
This can make permission checks consistent across the application.
Context API for Application Configuration
Context is useful for configuration that many components need.
For example:
const config = {
apiUrl: "https://api.example.com",
appName: "My Application",
version: "1.0"
};
Create:
const ConfigContext = createContext(null);
Provide:
<ConfigContext.Provider value={config}>
<App />
</ConfigContext.Provider>
Consume:
function UserService() {
const config = useContext(ConfigContext);
console.log(config.apiUrl);
}
This avoids passing configuration through many component layers.
Context API for Language / Localization
Suppose your application supports:
English
Tamil
Hindi
You could store the current language in Context.
const LanguageContext = createContext(null);
Provider:
function LanguageProvider({ children }) {
const [language, setLanguage] = useState("en");
const translations = {
en: {
welcome: "Welcome",
logout: "Logout"
},
ta: {
welcome: "வரவேற்கிறோம்",
logout: "வெளியேறு"
}
};
return (
<LanguageContext.Provider
value={{
language,
setLanguage,
translations
}}
>
{children}
</LanguageContext.Provider>
);
}
Then:
function Header() {
const {
language,
setLanguage,
translations
} = useContext(LanguageContext);
return (
<header>
<h2>{translations[language].welcome}</h2>
<button onClick={() => setLanguage("ta")}>
Tamil
</button>
</header>
);
}
This is a practical example of Context managing application-wide preferences.
Multiple Contexts
A real application may need several independent contexts.
For example:
App
│
├── AuthProvider
│ │
│ └── ThemeProvider
│ │
│ └── LanguageProvider
│ │
│ └── Application
Example:
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<App />
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
Components can consume only the context they need.
function Dashboard() {
const { user } = useAuth();
const { theme } = useTheme();
const { language } = useLanguage();
// use all three
}
This is common in larger applications.
Can One Context Use Another Context?
Yes.
For example, an application might have:
AuthContext
↓
PermissionContext
The PermissionProvider can consume authentication information.
Conceptually:
<AuthProvider>
<PermissionProvider>
<App />
</PermissionProvider>
</AuthProvider>
Inside PermissionProvider:
const { user } = useAuth();
Then it can derive permissions from the authenticated user.
This allows contexts to be composed.
Updating Context from Deep Child Components
This is an important concept.
Suppose:
App
│
└── Provider
│
└── Dashboard
│
└── Settings
│
└── Button
The Button can update context state:
const { setTheme } = useTheme();
It doesn't matter how deeply nested it is.
For example:
function Button() {
const { setTheme } = useTheme();
return (
<button onClick={() => setTheme("dark")}>
Dark Mode
</button>
);
}
The update travels back through the Context state:
Button
│
│ setTheme()
▼
Provider State
│
│ state changes
▼
Context Value
│
▼
Consumers re-render
Context with useReducer
For simple state, useState is usually sufficient.
But when shared state has multiple actions and complex transitions, useReducer can be combined with Context.
For example, a shopping cart might have:
ADD_ITEM
REMOVE_ITEM
INCREASE_QUANTITY
DECREASE_QUANTITY
CLEAR_CART
Instead of many state setters, use a reducer.
const initialState = {
items: []
};
Reducer:
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM":
return {
...state,
items: [...state.items, action.payload]
};
case "REMOVE_ITEM":
return {
...state,
items: state.items.filter(
item => item.id !== action.payload
)
};
case "CLEAR_CART":
return {
...state,
items: []
};
default:
return state;
}
}
Provider:
function CartProvider({ children }) {
const [state, dispatch] = useReducer(
cartReducer,
initialState
);
return (
<CartContext.Provider
value={{
state,
dispatch
}}
>
{children}
</CartContext.Provider>
);
}
Then any component can dispatch actions:
function ProductCard({ product }) {
const { dispatch } = useContext(CartContext);
return (
<button
onClick={() =>
dispatch({
type: "ADD_ITEM",
payload: product
})
}
>
Add to Cart
</button>
);
}
This pattern is:
Context
+
useReducer
↓
Shared predictable state management
Context + useReducer Pattern
This is one of the most powerful Context patterns.
Provider
│
useReducer()
│
┌───────┴───────┐
│ │
state dispatch
│ │
▼ ▼
Components Actions
│
▼
Reducer
│
▼
New State
For complex application state, this gives you a Redux-like architecture while using React's built-in features.
However, Context + useReducer is not automatically a replacement for Redux. Redux provides additional capabilities and a different architecture.
Splitting Contexts
One common mistake is putting everything into one huge context.
For example:
<AppContext.Provider
value={{
user,
theme,
language,
cart,
settings,
notifications,
permissions
}}
>
This can become difficult to maintain.
A better design is often:
AuthContext
ThemeContext
LanguageContext
CartContext
NotificationContext
Then:
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<CartProvider>
<App />
</CartProvider>
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
This provides better separation of responsibilities.
Context and Re-rendering
This is one of the most important concepts to understand.
Suppose:
const [theme, setTheme] = useState("light");
and:
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
When theme changes:
setTheme("dark")
↓
Provider re-renders
↓
Context value changes
↓
Components consuming ThemeContext
↓
Re-render
This is why Context should not automatically be treated as a replacement for every state-management problem.
The Context Value Object and Re-rendering
Consider:
<ThemeContext.Provider
value={{
theme,
setTheme
}}
>
The object passed to value is created during rendering.
For more complex applications, you may use useMemo to stabilize the context value when appropriate:
const value = useMemo(
() => ({
theme,
setTheme
}),
[theme]
);
Then:
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
The important point is not "always use useMemo with Context."
Instead:
Use memoization when unnecessary context-value identity changes are causing meaningful rendering problems.
Do not add useMemo everywhere without measuring or understanding the rendering behavior.
React 19 Provider Syntax
Modern React also supports rendering the context object itself as the provider.
Instead of:
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
you can use:
<ThemeContext value={value}>
{children}
</ThemeContext>
So the modern syntax is:
<ThemeContext value={value}>
{children}
</ThemeContext>
The older .Provider syntax remains important to understand because you will encounter it frequently in existing React codebases.
useContext vs Consumer Component
There are two major ways to consume Context.
Modern approach: useContext
function Profile() {
const user = useContext(UserContext);
return <h2>{user.name}</h2>;
}
This is the normal approach for functional components.
Older/render-prop approach: Consumer
function Profile() {
return (
<UserContext.Consumer>
{user => (
<h2>{user.name}</h2>
)}
</UserContext.Consumer>
);
}
The Consumer API is still valid, but useContext() is generally simpler and preferred when using function components.
Context Outside the Provider
Consider:
function App() {
return <Profile />;
}
and:
function Profile() {
const user = useContext(UserContext);
return <h2>{user.name}</h2>;
}
If there is no Provider, useContext() uses the default value supplied to createContext().
For example:
const UserContext = createContext({
name: "Guest"
});
Then:
const user = useContext(UserContext);
returns:
{
name: "Guest"
}
This behavior can be useful for defaults, but it can also hide configuration errors.
Making a Custom Hook Throw an Error
For required contexts, a useful pattern is:
const AuthContext = createContext(null);
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error(
"useAuth must be used inside AuthProvider"
);
}
return context;
}
Now if someone accidentally writes:
function Dashboard() {
const auth = useAuth();
}
without:
<AuthProvider>
React will throw a meaningful error.
This is a very useful production pattern.
Context API with TypeScript
Since you are working with React + TypeScript, typing Context correctly is important.
For example:
type User = {
id: number;
name: string;
email: string;
};
type UserContextType = {
user: User | null;
login: (user: User) => void;
logout: () => void;
};
Create the context:
const UserContext =
createContext<UserContextType | undefined>(undefined);
Then:
function UserProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
const login = (user: User) => {
setUser(user);
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider
value={{
user,
login,
logout
}}
>
{children}
</UserContext.Provider>
);
}
Custom hook:
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error(
"useUser must be used inside UserProvider"
);
}
return context;
}
Now:
function Profile() {
const { user, logout } = useUser();
return (
<div>
<h2>{user?.name}</h2>
<button onClick={logout}>
Logout
</button>
</div>
);
}
This is a strong pattern for TypeScript applications.
Context API with null vs undefined
You will often see:
createContext(null)
or:
createContext(undefined)
With TypeScript, this pattern is particularly useful:
createContext<UserContextType | undefined>(undefined);
Then the custom hook verifies:
if (!context) {
throw new Error(...);
}
This makes it clear that the Context must be supplied by a Provider.
Context API for Global Modal
Suppose your application has multiple components that need to open a modal.
Instead of:
setModalOpen(...)
being passed through multiple components, create:
ModalContext
Provider:
function ModalProvider({ children }) {
const [isOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
return (
<ModalContext.Provider
value={{
isOpen,
openModal,
closeModal
}}
>
{children}
{isOpen && <Modal />}
</ModalContext.Provider>
);
}
Any descendant can now do:
const { openModal } = useModal();
and open the global modal.
This is useful for:
- Confirmation dialogs
- Global alerts
- Login dialogs
- Delete confirmation
- Notification panels
Context API for Notifications
Another practical example is a global notification system.
For example:
const NotificationContext = createContext(null);
Provider:
function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const notify = message => {
setNotifications(current => [
...current,
{
id: Date.now(),
message
}
]);
};
return (
<NotificationContext.Provider
value={{
notifications,
notify
}}
>
{children}
</NotificationContext.Provider>
);
}
Any component can now do:
const { notify } = useNotification();
notify("User saved successfully");
This avoids passing notification functions through many layers.
Context API for Form State
Context can also be useful for large forms.
For example:
Checkout
├── CustomerInformation
├── Address
├── Payment
└── OrderSummary
All these components may need access to the same form state.
Instead of passing:
formData
setFormData
validation
errors
through every level, a Form Context can provide them.
<FormProvider>
<Checkout />
</FormProvider>
Then:
function Payment() {
const {
formData,
updateField
} = useCheckout();
}
This is particularly useful when a form is split across many components.
Context API vs Props
| Feature | Props | Context |
|---|---|---|
| Data passed explicitly | Yes | No |
| Prop drilling | Possible | Avoided |
| Best for parent → child | Yes | Not necessary |
| Best for deeply shared data | Less convenient | Yes |
| Component dependency | Clear | More implicit |
| Local component state | Excellent | Usually unnecessary |
| Application-wide settings | Possible but cumbersome | Good |
A good rule is:
Use props when data belongs to a parent-child relationship. Use Context when many components at different levels need the same data.
Context API vs useState
These solve different problems.
useState manages state:
const [theme, setTheme] = useState("light");
Context distributes data:
<ThemeContext.Provider value={theme}>
They are often used together:
useState
↓
State ownership
↓
Context
↓
State distribution
↓
Child components
So Context itself isn't a replacement for useState.
Context API vs useReducer
useReducer manages complex state transitions.
Context distributes the resulting state/actions.
Therefore:
useReducer
+
Context
=
Shared complex state
Example:
const [state, dispatch] = useReducer(reducer, initialState);
<Context.Provider value={{ state, dispatch }}>
This is a powerful combination.
Context API vs Redux
Context and Redux are related but not identical.
Context
Context primarily solves:
How can components share data without prop drilling?
Redux
Redux provides a broader state-management architecture involving:
- Centralized state
- Actions
- Reducers
- Store
- Middleware
- DevTools
- Predictable state transitions
- Ecosystem support
For simple shared state:
Context + useState
may be enough.
For complex application-wide state:
Context + useReducer
may work well.
For very large applications with sophisticated state requirements:
Redux Toolkit
may be more appropriate.
When Should You Use Context API?
Context is a good choice when:
- Many components need the same data.
- The components are deeply nested.
- Passing props would create significant prop drilling.
- The data represents application-level configuration.
- You need shared authentication information.
- You need theme information.
- You need localization settings.
- You need shared cart state.
- You need shared permissions.
- You need global UI state such as modal/notification state.
When Should You NOT Use Context?
Context should not become your default solution for every piece of state.
For example:
const [name, setName] = useState("");
If only one component needs name, Context provides no benefit.
Don't create:
NameContext
just because you can.
Similarly, if a parent directly renders a child:
function Parent() {
const user = ...;
return <Profile user={user} />;
}
using props is often better.
The relationship is explicit:
Parent
↓ props
Profile
There is no reason to introduce Context.
Context Provider Placement Matters
Consider:
<ThemeProvider>
<App />
</ThemeProvider>
Every component inside App can consume the theme.
But you can also scope the Provider:
<App>
<ThemeProvider>
<AdminPanel />
</ThemeProvider>
<PublicPage />
</App>
Now:
AdminPanel
↓
can access ThemeContext
PublicPage
↓
cannot access this ThemeContext
This is useful when a context should apply only to a particular section.
Multiple Providers Can Have Different Values
You can even have multiple instances of the same Context.
<ThemeContext.Provider value="dark">
<AdminPanel />
<ThemeContext.Provider value="light">
<PublicPanel />
</ThemeContext.Provider>
</ThemeContext.Provider>
The nearest Provider wins.
So:
AdminPanel
→ dark
PublicPanel
→ light
This demonstrates that Context is scoped to the component tree.
Nearest Provider Wins
Suppose:
<ThemeContext.Provider value="dark">
<Dashboard />
<ThemeContext.Provider value="light">
<Profile />
</ThemeContext.Provider>
</ThemeContext.Provider>
Then:
Dashboard → dark
Profile → light
Profile uses the closest Provider.
This is an important rule when debugging Context behavior.
Context and Component Reusability
Context can sometimes make reusable components more tightly coupled to application infrastructure.
For example:
function ProductCard() {
const { addToCart } = useCart();
// ...
}
This component now expects CartProvider to exist.
Compare that with:
function ProductCard({ product, onAddToCart }) {
// ...
}
The second component is more explicitly reusable because its dependency is visible through props.
Therefore:
Context improves convenience, but props can improve component independence and reusability.
This is one reason not to use Context everywhere.
A Production-Style Context Architecture
A scalable React application might have:
src/
│
├── contexts/
│ ├── AuthContext.tsx
│ ├── ThemeContext.tsx
│ └── CartContext.tsx
│
├── hooks/
│ ├── useAuth.ts
│ ├── useTheme.ts
│ └── useCart.ts
│
├── components/
│ ├── Header.tsx
│ ├── Sidebar.tsx
│ └── ProductCard.tsx
│
└── App.tsx
And:
<AuthProvider>
<ThemeProvider>
<CartProvider>
<App />
</CartProvider>
</ThemeProvider>
</AuthProvider>
Components consume only what they need:
const { user } = useAuth();
or:
const { theme } = useTheme();
or:
const { cart, addToCart } = useCart();
This keeps the application organized.
The Most Important Context API Pattern
If you remember only one pattern, remember this:
const Context = createContext<ContextType | undefined>(
undefined
);
function Provider({ children }) {
const [state, setState] = useState(initialState);
const value = {
state,
setState
};
return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);
}
function useCustomContext() {
const context = useContext(Context);
if (!context) {
throw new Error(
"useCustomContext must be used inside Provider"
);
}
return context;
}
Usage:
function App() {
return (
<Provider>
<Dashboard />
</Provider>
);
}
Consumer:
function Dashboard() {
const { state, setState } = useCustomContext();
// use shared state
}
This pattern covers a huge percentage of real-world Context API usage.
Complete Mental Model
The easiest way to understand Context is to separate the responsibilities:
createContext()
│
│
▼
Context
│
│
Provider supplies value
│
▼
Component Tree
│
┌────────┼────────┐
│ │ │
▼ ▼ ▼
Header Dashboard Profile
│ │ │
└────────┼────────┘
│
useContext()
│
▼
Shared Value
If the value contains state:
useState / useReducer
│
▼
Provider
│
▼
Context
│
▼
Consumers
If a consumer changes the state:
Consumer
│
│ setState() / dispatch()
▼
Provider state
│
▼
Context value changes
│
▼
Consumers receive updated value
│
▼
Re-render
Context API — Key Points to Remember
- Context solves data-sharing problems, especially prop drilling.
createContext()creates the Context.- A Provider supplies the value.
useContext()reads the value.- Context values can be simple values, objects, functions, state, or reducer state.
- Context can be combined with
useStatefor shared state. - Context can be combined with
useReducerfor complex shared state. - A custom hook such as
useAuth()oruseTheme()makes Context easier to consume. - Context is not a JavaScript global variable; it is scoped to a Provider's component subtree.
- The nearest Provider wins when multiple Providers exist.
- Context should not replace props for ordinary parent-child communication.
- Context should not be used for every piece of state.
- Splitting large contexts into smaller contexts can improve architecture and performance.
- Context consumers update when the context value they consume changes.
- A common production pattern is:
Context
+
Provider
+
useState / useReducer
+
Custom Hook
That combination gives you a clean, reusable way to manage shared state in React without introducing a third-party state-management library.