As the MERN Blog grows, certain data needs to be accessible in many components at many different levels of the component tree โ the currently logged-in user, the active theme, a notification count. Passing this data as props through every intermediate component is called prop drilling, and it quickly makes the codebase tedious and fragile. React Context is the built-in solution: it lets you make data available to any component in the tree without threading it through props. Understanding when Context is the right tool โ and when it is overkill โ is the key decision this lesson addresses.
The Prop Drilling Problem
// Without Context: user passed as prop through every intermediate component
function App() {
const [user, setUser] = useState(null);
return <PageLayout user={user} setUser={setUser} />;
}
function PageLayout({ user, setUser }) {
return (
<div>
<Header user={user} setUser={setUser} /> {/* passes down */}
<main><Outlet /></main>
</div>
);
}
function Header({ user, setUser }) {
return <nav><UserMenu user={user} setUser={setUser} /></nav> {/* passes down */}
}
function UserMenu({ user, setUser }) {
// Only this component actually uses user โ all the above were just passing it
return user
? <span>{user.name} <button onClick={() => setUser(null)}>Log Out</button></span>
: <Link to="/login">Log In</Link>;
}
// Problem: PageLayout and Header know about user/setUser but never use it
// Adding a new prop means updating every component in the chain
Context vs Props vs Lifting State โ Decision Guide
| Scenario | Best Approach |
|---|---|
| Two sibling components share state | Lift state to common parent, pass as props |
| State passes through 3+ components that do not use it | React Context |
| Current logged-in user โ needed everywhere | React Context (AuthContext) |
| Active theme โ affects most components | React Context (ThemeContext) |
| A form’s field values โ local to the form | Local useState inside the form |
| Large, complex global state with many actions | Zustand, Redux Toolkit, or Jotai |
| Server state (API data, caching, background refetch) | TanStack Query (React Query) |
How Context Works โ Mental Model
Without Context:
App โ PageLayout โ Header โ UserMenu
data flows through props at every step โ all components know about user
With Context:
AuthProvider wraps the tree
โ
โโโ PageLayout (does not touch user โ transparent)
โ โโโ Header (does not touch user โ transparent)
โ โ โโโ UserMenu โ useAuth() reads user directly
โ โโโ PostDetailPage โ useAuth() reads user directly
โโโ DashboardPage โ useAuth() reads user directly
Components that need user read it directly from context
Components that don't need user are completely unaware of it
What Belongs in the MERN Blog Contexts
| Context | What It Holds | Who Consumes It |
|---|---|---|
| AuthContext | user, token, login(), logout(), register(), loading | Header, ProtectedRoute, PostCard (show edit if owner), ProfilePage |
| ThemeContext | theme (‘light’/’dark’), toggleTheme() | Header (toggle button), any component with theme-dependent styling |
| NotificationContext | notifications[], addNotification(), dismissNotification() | NotificationBanner, any component that triggers notifications |
Common Mistakes
Mistake 1 โ Using Context for data that only two components share
โ Wrong โ creating a context for a search query shared between SearchBar and PostList:
// SearchContext.jsx โ overkill for two adjacent siblings
// Just lift query to BlogPage and pass as props โ
โ Correct โ lift state to the nearest common ancestor for nearby components.
Mistake 2 โ Putting fast-changing data in a broad context
โ Wrong โ putting the post list in AuthContext alongside user data:
// Every time a post is liked, ALL context consumers re-render (including Header!)
const AuthContext = createContext({ user, posts, likedPostIds, ... });
โ Correct โ keep AuthContext focused on auth state only; post data belongs in page-level state.
Mistake 3 โ Reading context outside a Provider
โ Wrong โ using useAuth() in a component rendered above AuthProvider:
// App.jsx โ AuthProvider is NOT a parent of this component
function RootHeader() {
const { user } = useAuth(); // undefined โ no Provider above this component
return <header>{user?.name}</header>;
}
// Used above <AuthProvider> in the tree
โ Correct โ every component that consumes a context must be a descendant of its Provider.
Quick Reference
| Concept | Key Point |
|---|---|
| Context purpose | Eliminate prop drilling for truly global data |
| When to use | Prop passes through 3+ components that do not use it |
| When NOT to use | Two nearby components sharing state โ lift instead |
| Re-render scope | All consumers re-render when context value changes |
| Alternative for complex state | Zustand, Redux Toolkit |
| Alternative for server state | TanStack Query |