When developers first start building with React, passing data around feels simple. You keep state in a parent component, pass it down as props, and everything works. But as your application grows, you often notice something frustrating: props being passed through layers of components that don’t even use them. This is known as prop drilling, and while it works, it quickly becomes hard to manage.
To address this, React introduced the Context API, which allows you to share state across components without threading props through every level of the tree. Both approaches have their place, but knowing when to use one over the other is key to writing clean, maintainable React applications.
In this article, we’ll explore both patterns with examples, compare their tradeoffs, and highlight best practices.
What is Prop Drilling?
Prop drilling is the process of passing props from a parent component down to nested child components, even if intermediate components don’t use them.
Example
function App() {
const user = { name: "Alex", role: "Admin" };
return <Dashboard user={user} />;
}
function Dashboard({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Profile user={user} />;
}
function Profile({ user }) {
return <p>Welcome, {user.name}! You are logged in as {user.role}.</p>;
}
Here, both Dashboard
and Sidebar
are forced to receive user
as props just so Profile
can access it.
This works fine when the component tree is small. But as applications grow, prop drilling causes:
Verbose code: components carry props they don’t actually need.
Reduced maintainability: if the shape of user
changes, you update every layer that passes it.
Brittle structure: refactoring components becomes painful because you must preserve the prop chain.
What is React Context?
React Context is designed to share values across the component tree without prop drilling. Instead of manually passing props, you wrap components with a Provider
and access the value using useContext
.
Here’s the previous example rewritten with Context:
import React, { createContext, useContext } from "react";
const UserContext = createContext();
function App() {
const user = { name: "Alex", role: "Admin" };
return (
<UserContext.Provider value={user}>
<Dashboard />
</UserContext.Provider>
);
}
function Dashboard() {
return <Sidebar />;
}
function Sidebar() {
return <Profile />;
}
function Profile() {
const user = useContext(UserContext);
return <p>Welcome, {user.name}! You are logged in as {user.role}.</p>;
}
With Context, intermediate components like Dashboard
and Sidebar
don’t need to know anything about user
. Profile
can access it directly.
Real-World Scenarios
Prop Drilling Works Best For
Local state: Data only passed through a couple of components.
Explicitness: You want to see where props come from.
Reusable components: Passing props intentionally makes components more flexible.
Example
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
Here, prop drilling isn’t a problem. The data is localized, and the component is reusable.
Context Works Best For
Global settings: Theme, language, and authentication state.
Deeply nested components: Data needed by children several levels down.
Cross-cutting concerns: Values that many components need access to.
Example: theme management
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Navbar />
</ThemeContext.Provider>
);
}
function Navbar() {
const theme = useContext(ThemeContext);
return <nav className={theme === "dark" ? "dark-nav" : "light-nav"}>Menu</nav>;
}
Without Context, you’d have to pass theme
through every component between App
and Navbar
.
Common Mistakes Developers Make
Overusing Context
Developers sometimes put every piece of state into Context. This leads to unnecessary re-renders across the tree and makes debugging harder. Context should only be used for a state that is truly global.
Mismanaging large Contexts
If you put too much unrelated data into a single Context, any update will cause all consumers to re-render. The solution is to split your Contexts into smaller, focused ones (e.g., AuthContext
, ThemeContext
, SettingsContext
).
Avoiding Props completely
Props are still important! They make data flow explicit. Use Context only when props become painful.
Best Practices
Start with props. Don’t jump to Context prematurely. If props get too deep, refactor.
Use multiple Contexts. Keep data separated by concern.
Combine Context with reducers. For example, useReducer
inside Context makes managing global state more predictable.
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, dispatch] = React.useReducer(authReducer, null);
return (
<AuthContext.Provider value={{ user, dispatch }}>
{children}
</AuthContext.Provider>
);
}
Conclusion
Both prop drilling and React Context are valid ways of sharing data between components. Prop drilling keeps data flow explicit and works well for small, localized states. Context provides a cleaner way to share global state across deeply nested components, but it can become tricky if overused.
A balanced approach often works best: use props for local state and Context for global concerns like authentication, theme, or settings. As your app grows, you may even combine Context with external state management tools like Redux, Zustand, or Jotai for more complex scenarios.
The key takeaway is this: choose the simplest tool that solves your problem without overcomplicating the codebase. Start with props, reach for Context when drilling gets messy, and don’t be afraid to mix strategies when your application demands it.