What Is Prop Drilling and How to Avoid It
Advertisement
🧩 Scenario
Architecture Walkthrough
What Prop Drilling Is
Prop drilling occurs when data is passed through intermediate components that do not use it, only to reach a deeply nested consumer. Each intermediate component is coupled to the data: it must declare the prop in its interface and forward it explicitly. If the data shape changes or the consumer moves in the tree, every intermediary must be updated. Two or three levels of drilling is normal React; four or more levels usually signals a design problem.
The problem is not performance (React is fast at passing props), it is maintainability. Intermediate components become brittle, and the data's path through the tree is not obvious from any single file.
Solution 1: Component Composition
The most underused solution is component composition. Instead of passing data down through intermediaries, pass the component that needs the data as a children prop or as a named slot prop. The parent that owns the data renders the consumer directly and passes the data there, without threading it through intermediate components.
This is the solution React's own documentation recommends first. It requires no new API and no additional state management infrastructure.
Solution 2: Context API
When the same data needs to be available to many unrelated components scattered across the tree (user session, theme, locale, feature flags), Context is the right tool. The Provider wraps the subtree at the appropriate level and any descendant reads the value with useContext. No intermediate component needs to know about the data.
The tradeoff is that all consumers re-render when the context value changes. Split contexts by update frequency to mitigate this.
Solutions 3 and 4: State Managers and Custom Hooks
Zustand, Jotai, and Redux Toolkit provide stores that components subscribe to directly, with selector-based subscriptions that prevent unnecessary re-renders. They are appropriate when many components across the app share state that updates frequently. Custom hooks that use useContext or store APIs provide a clean API layer regardless of which solution is used underneath.
Key Code Explained
// The problem: prop drilling through three layers
function App() {
const user = useCurrentUser();
return <Layout user={user} />;
}
function Layout({ user }: { user: User }) {
return <Sidebar user={user} />; // Layout doesn't use user
}
function Sidebar({ user }: { user: User }) {
return <UserMenu user={user} />; // Sidebar doesn't use user
}
function UserMenu({ user }: { user: User }) {
return <p>Hello, {user.name}</p>; // Only UserMenu uses user
}
// Solution 1: Component composition — pass the consumer, not the data
function App() {
const user = useCurrentUser();
// App renders UserMenu directly and passes user here, not through Layout/Sidebar
return <Layout sidebar={<UserMenu user={user} />} />;
}
function Layout({ sidebar }: { sidebar: React.ReactNode }) {
return (
<div>
<aside>{sidebar}</aside>
<main>...</main>
</div>
);
}
function Sidebar() {
// No user prop needed — just renders children
return <div className="sidebar">{/* slots filled by parent */}</div>;
}
function UserMenu({ user }: { user: User }) {
return <p>Hello, {user.name}</p>;
}
// Layout and Sidebar are no longer coupled to User at all
// Solution 2: Context API
const UserContext = createContext<User | null>(null);
function useUser() {
const user = useContext(UserContext);
if (!user) throw new Error('useUser must be inside UserProvider');
return user;
}
function App() {
const user = useCurrentUser();
return (
<UserContext.Provider value={user}>
<Layout /> {/* no user prop */}
</UserContext.Provider>
);
}
function Layout() {
return <Sidebar />; // no user prop
}
function Sidebar() {
return <UserMenu />; // no user prop
}
function UserMenu() {
const user = useUser(); // reads from context directly
return <p>Hello, {user.name}</p>;
}
// Solution 3: Zustand (lightweight state manager)
import { create } from 'zustand';
interface UserStore {
user: User | null;
setUser: (user: User) => void;
}
const useUserStore = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
function UserMenu() {
const user = useUserStore((state) => state.user); // fine-grained subscription
return <p>Hello, {user?.name}</p>;
}
// Only re-renders when user changes, not when other store values change
The component composition solution is the most important one to demonstrate in an interview because it shows you understand React's core model before reaching for Context. Many prop drilling problems can be solved by passing JSX or components down instead of data, which requires no additional infrastructure.
Tradeoffs
| Solution | Complexity | Re-render cost | When to use |
|---|---|---|---|
| Prop drilling (1-2 deep) | None | None | Default React for shallow trees |
| Composition | Low | None | When consumer can be rendered by the data owner |
| Context | Low-medium | All consumers | Shared data for many components at various depths |
| Zustand / Redux | Medium | Only subscribers | Frequently updated state shared app-wide |
What Interviewers Actually Check
- Whether you can identify prop drilling and explain the maintainability problem
- Whether you know component composition as the first solution to try
- Whether you know Context and its re-render cost
- Whether you know state managers as the appropriate tool for high-frequency, app-wide state
- Whether you can articulate a decision framework rather than defaulting to one solution
Follow-Up Questions
- How does the
childrenprop enable component composition as a prop drilling solution? - If Context re-renders all consumers on value change, how does
use-context-selectorsolve this, and how does it work internally? - When would you prefer
useReducer+ Context overuseState+ Context for sharing actions alongside data? - How does React Query or SWR change the prop drilling story for server data specifically?
- What is the compound component pattern and how does it use Context internally to avoid prop drilling within a component family?
Common Candidate Mistakes
- Reaching for Context as the solution to any prop passing beyond one level, before considering composition
- Not knowing that Context has a re-render cost and treating it as a zero-cost substitute for props
- Installing a state management library (Redux) for data only three components need
- Prop drilling both data and callbacks when both could be moved to the same Context
- Not articulating why prop drilling becomes a problem only at three or more levels of intermediate components
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you identify a prop drilling problem and articulate why it becomes a maintenance issue?
- Can you refactor a prop-drilled tree using component composition?
- Can you lift data to Context and consume it with a custom hook without prop drilling?
- Can you describe the re-render cost of Context and when it matters?
- Can you articulate a decision framework: composition first, Context second, state manager third?
Summary
Prop drilling is the pattern of passing props through intermediate components that do not use them, only to reach a deeply nested consumer. One or two levels of prop passing is idiomatic React; three or more levels creates coupling between intermediate components and the data, making refactors painful and the data flow opaque.
The first solution to reach for is component composition: instead of threading data through intermediaries, the component that owns the data renders the consumer and passes data there directly. No intermediate component needs to know about the data. This requires no new infrastructure and is often the simplest fix.
Context is the right tool when the same data must be available to many unrelated consumers across the tree (user session, theme, locale). Any descendant can read the value with useContext without any intermediate component needing to forward it. The tradeoff is that all consumers re-render when the context value changes. For frequently updated, app-wide state with many consumers, a dedicated state manager like Zustand provides selector-based subscriptions that prevent unnecessary re-renders.
Is prop drilling always bad?
No. Passing props one or two levels deep is idiomatic React. Prop drilling becomes a problem when props must pass through three or more intermediate components that do not use them, creating coupling and making refactors painful.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement