Prop drilling is one of those terms that gets thrown around in React codebases as a kind of architectural smell — “oh, we’re drilling too deep here.” But not all drilling is bad. Sometimes it's the simplest way to pass data down.
What is bad is when you do it accidentally, inconsistently, or without recognizing that your component tree is signaling a structural problem.
Let’s look at how to avoid prop drilling not through tools, but through conventions.
Lift state only one level above where it’s needed
Many engineers hear “lift state up” and interpret that as “hoist to the top of the app.” But in practice, state should live at the lowest common owner.
Bad:
<App>
<Page user={user}>
<Header user={user} />
<Content user={user}>
<Sidebar user={user} />
</Content>
</Page>
</App>
Better:
<Page>
<Header />
<Content>
<Sidebar />
</Content>
</Page>
...and inside Page
:
function Page() {
const user = useUser(); // maybe from context or a hook
return (
<UserProvider value={user}>
<Header />
<Content />
</UserProvider>
);
}
This is simpler not because we added a context, but because we localized responsibility. Lift state only to the level that needs to coordinate.
đź§± Create slice-specific context, not global context
Context is easy to misuse. A common mistake is to make one global AppContext
that contains everything.
That’s just prop drilling in disguise. You’re still pulling values out at the leaf node and passing them around via a bag of stuff.
Instead, define feature-specific context.
// inside auth/AuthContext.tsx
export const AuthContext = createContext<User | null>(null);
Then only the subtree that needs it consumes it.
If you find yourself calling useContext(AppContext)
in 15 different files, you’ve made your state global when it wasn’t.
đź§ą Use compound components to encapsulate shared state
A powerful React pattern is the compound component. This is a way to co-locate components that rely on shared state, without exposing that state manually.
Example:
<Accordion>
<Accordion.Item>
<Accordion.Header>Title</Accordion.Header>
<Accordion.Body>Content</Accordion.Body>
</Accordion.Item>
</Accordion>
Internally, Accordion.Item
provides context, and Accordion.Header
and Accordion.Body
consume it.
You don’t need to pass down the state manually because you’ve scoped it with structure.
This lets you avoid prop drilling by design — because your components only make sense in a certain tree.
📦 Bundle multiple values into one object prop
Sometimes drilling is okay, but you can reduce the pain by grouping related values together.
Instead of:
<MyComponent name={name} email={email} avatarUrl={avatarUrl} />
Just:
<MyComponent user={user} />
Yes, it’s obvious. But many codebases forget this — and you start seeing 4–5 sibling props all derived from the same parent.
Even better: pass a stable object from a custom hook:
const user = useUser(); // { name, email, avatarUrl }
<MyComponent user={user} />
đź§ Think in terms of ownership, not access
Prop drilling often happens because you have components accessing data they don’t own.
But in React, it’s helpful to design components that own their slice of state and only expose what’s needed. This is the opposite of treating components as pure render templates.
If Sidebar
needs user preferences, maybe Sidebar
should own that state. Or maybe a higher-order component like <WithPreferences>
wraps it.
When every component tries to be stateless and pure, you end up with pipelines of props.
Sometimes, you need to give up purity for locality.
✨ TL;DR
Avoiding prop drilling is less about magic tools and more about thoughtful structure. Here are the conventions that help:
- Don’t lift state too far — just to the common owner
- Scope context to a feature, not to the whole app
- Use compound components to share local state
- Bundle props into objects to reduce noise
- Design with ownership, not just access
Prop drilling isn’t the enemy. Unintentional structure is.