Context API is a mechanism that React provides to share data between components without having to pass props manually through every level of the component tree.
In the previous article, we saw a common problem in React Apps: Prop Drilling. We saw that passing data through 10 components that don’t need it is inefficient and hard to maintain.
Instead, Context API allows components to access data directly, no matter where they are in the tree.
Context is like a broadcasting system
- We have a Broadcaster (Provider) at the top of the application that emits data.
- Any component, wherever it is in the tree, can tune into that broadcaster using a Receiver (Consumer) and access the data directly.
When to Use Context?
Context is very practical, but it has a very high performance cost on re-renders. Every time the Context value changes (e.g., we change the theme), ALL components consuming that context re-render.
✅ Context is ideal for
- Infrequently changing data: Theme, Language, Authenticated User.
- Static configuration data.
❌ Not recommended for
- High-frequency state: Keyboard inputs, mouse positions, real-time data changing 60 times per second. For that, we would cause tremendous lag in the app.
How does Context API work?
Context API is based on three main concepts:
Create the context (createContext)
Context is an object containing the data you want to share.
Provide the value (Provider)
It’s a component that wraps the components that need access to the context. It provides data to its child components.
Consume the value (useContext)
It’s a Hook that allows child components to access the context data.
Let’s implement the most classic and useful example to understand this: A Theme Manager (Light / Dark).
Step 1: The “Custom Provider” Pattern
Although we could create the context directly in App.jsx, we are going to create a dedicated file to manage all the theme logic.
Create src/context/ThemeContext.jsx:
import { createContext, useState, useContext } from 'react';
// 1. CREATE THE CONTEXT
// The initial value (null) is only used if we try to access the context
// WITHOUT a provider (useful for tests, but rare in production).
const ThemeContext = createContext(null);
// 2. CREATE THE PROVIDER COMPONENT (The "Cloud")
export function ThemeProvider({ children }) {
// Here we store the global state
const [theme, setTheme] = useState('light'); // 'light' | 'dark'
const toggleTheme = () => {
setTheme((curr) => (curr === 'light' ? 'dark' : 'light'));
};
// The value we will share with the entire app
const contextValue = {
theme,
toggleTheme
};
return (
// We wrap the children with the Provider and pass it the value
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}
// 3. CUSTOM HOOK FOR CONSUMPTION (The "Antenna")
// This is a good practice to avoid having to import useContext and the Context
// in every component. We abstract the logic here.
export const useTheme = () => {
const context = useContext(ThemeContext);
// Safety validation:
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
All the theme logic (states and functions to change it) lives here, isolated from the rest of the app.
Step 2: Connect the Provider
Now we need to go to the root of our application (usually main.jsx or App.jsx) and wrap everything with our ThemeProvider.
// src/App.jsx
import { ThemeProvider } from './context/ThemeContext';
import Layout from './components/Layout';
function App() {
return (
// All components inside ThemeProvider will have access to the theme
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
Step 3: Consume the data
Now comes the interesting part. Imagine a button that is deep in the hierarchy, inside Layout ➡ Header ➡ UserMenu ➡ ThemeButton.
We no longer need to pass props. We simply use our useTheme hook.
// src/components/ThemeButton.jsx
import { useTheme } from '../context/ThemeContext';
export default function ThemeButton() {
// We "teleport" the data and the function here
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
}}
>
Switch to {theme === 'light' ? 'Dark' : 'Light'} mode
</button>
);
}
We have connected the button to the global state without touching any intermediate component 🎉.
Multiple Contexts
We can nest as many Providers as we want. It’s common to see apps like this:
<AuthProvider>
<ThemeProvider>
<LanguageProvider>
<App />
</LanguageProvider>
</ThemeProvider>
</AuthProvider>
This structure keeps the logic separate: the theme knows nothing about the user, and the user knows nothing about the language.
