When you start building React applications, one of the first challenges you’ll face is managing state across multiple components. Passing props down through multiple levels (known as prop drilling) quickly becomes messy. That’s where the React Context API comes to the rescue.
In this blog, we’ll break down Context API step by step with definitions, explanations, and code examples.
1. React.createContext → Creating Context
Definition:
React.createContext is used to create a Context object that allows data to be shared across the component tree without manually passing props at every level.
Explanation:
When you create a context, React gives you two important things:
Provider → supplies the data
Consumer → retrieves the data
Example:
import React, { createContext } from "react";
// Step 1: Create a Context
const ThemeContext = createContext();
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
return (
<ThemeContext.Consumer>
{(value) => <button>Theme is {value}</button>}
</ThemeContext.Consumer>
);
}
Here, ThemeContext allows us to pass the theme ("dark") down to ThemedButton without manually passing it through Toolbar.
2. Provider vs Consumer → Passing and Consuming Context Data
Provider:
Wraps part of the component tree and provides context data.
Takes a value prop which will be available to all child components.
Consumer:
Accesses context data inside the component.
Works with a function that receives the context value.
Example using Provider and Consumer:
const UserContext = createContext();
function App() {
return (
<UserContext.Provider value={{ name: "Tushar", role: "Admin" }}>
<Profile />
</UserContext.Provider>
);
}
function Profile() {
return (
<UserContext.Consumer>
{(user) => <h2>{user.name} is {user.role}</h2>}
</UserContext.Consumer>
);
}
This avoids passing user as props through multiple components.
👉 In modern React, we often use the useContext hook instead of Consumer.
Example with useContext:
import React, { useContext } from "react";
function Profile() {
const user = useContext(UserContext);
return <h2>{user.name} is {user.role}</h2>;
}
3. Avoiding Prop Drilling → Replacing Deep Prop Passing
Prop Drilling Problem:
Without context, you may end up passing props through intermediate components that don’t even need them.
Example of Prop Drilling (❌ Bad Practice):
function App() {
const theme = "dark";
return <Toolbar theme={theme} />;
}
function Toolbar({ theme }) {
return <ThemedButton theme={theme} />;
}
function ThemedButton({ theme }) {
return <button>Theme is {theme}</button>;
}
Here, theme has to pass through Toolbar, even though Toolbar doesn’t use it.
Using Context (✅ Good Practice):
const ThemeContext = createContext();
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button>Theme is {theme}</button>;
}
Now, no need to pass theme manually at every level! 🎉
4. Context Performance Issues → Unnecessary Re-renders
While Context is powerful, it comes with a performance caveat.
Problem:
Whenever the value of a Provider changes, all child components consuming that context will re-render — even if they don’t use the part of data that changed.
Example:
const UserContext = createContext();
function App() {
const [user, setUser] = React.useState({ name: "Tushar", role: "Admin" });
return (
<UserContext.Provider value={user}>
<Profile />
<Dashboard />
</UserContext.Provider>
);
}
If user.name changes, both Profile and Dashboard re-render, even if Dashboard only needed role.
✅ Solution: Use Memoization with Context
We can optimize using:
Splitting Contexts → Create separate contexts for different values.
React.memo → Prevents unnecessary re-renders for child components.
useMemo with Provider value → Ensures stable reference.
Optimized Example:
const UserContext = createContext();
function App() {
const [name, setName] = React.useState("Tushar");
const [role, setRole] = React.useState("Admin");
// useMemo to avoid re-creating object on every render
const userValue = React.useMemo(() => ({ name, role }), [name, role]);
return (
<UserContext.Provider value={userValue}>
<Profile />
<Dashboard />
</UserContext.Provider>
);
}
This way, React won’t trigger unnecessary re-renders unless the context value actually changes.
🎯 Final Thoughts
*createContext gives you a way to share data globally without prop drilling.
*Provider supplies data, Consumer / useContext reads it.
*Avoid Prop Drilling → Context makes state management simpler.
*Performance Tip: Always memoize context values to avoid unnecessary renders.
👉 Context API is great for small to medium apps, but for large-scale state management, consider Redux, Zustand, or Recoil.
Comments
Post a Comment