Don't pass props through 3+ intermediate components that don't use them. Use context, composition, or state management instead. Prop drilling creates tight coupling between distant components and makes refactoring painful — changing a prop type requires updating every component in the chain.
Prop drilling creates invisible coupling between components that should be independent. When a prop passes through 3+ layers, every intermediate component must declare it in its type signature, forward it correctly, and be updated if the prop shape changes. This makes refactoring expensive, increases the blast radius of changes, and obscures which components actually consume the data.
BeforeMerge scans your pull requests against this rule and 5+ others. Get actionable feedback before code ships.
Prop drilling is the practice of passing data through multiple layers of components that don't use the data themselves — they just forward it to children. With 3 or more intermediate layers, several problems compound:
First, every intermediate component is tightly coupled to data it doesn't need. If the prop's type changes, you must update every component in the chain, even though most of them never read the value.
Second, it obscures data flow. When reading a component, you see a prop being passed but can't tell if this component uses it or just forwards it. This makes code reviews and debugging significantly harder.
Third, it makes component reuse nearly impossible. An intermediate component that accepts and forwards currentUser, theme, locale, and permissions can't be used in any context that doesn't provide all of those props.
When data needs to reach a component more than 2 levels deep, replace prop drilling with one of:
// 4 levels of drilling — Dashboard > Sidebar > UserSection > UserAvatar
function Dashboard({ currentUser }: { currentUser: User }) {
return (
<div>
<Sidebar currentUser={currentUser} />
<MainContent />
</div>
);
}
function Sidebar({ currentUser }: { currentUser: User }) {
// Sidebar doesn't use currentUser — just forwards it
return (
<nav>
<NavLinks />
<UserSection currentUser={currentUser} />
</nav>
);
}
function UserSection({ currentUser }: { currentUser: User }) {
// UserSection doesn't use currentUser either — just forwards it
return (
<div>
<UserAvatar currentUser={currentUser} />
<LogoutButton />
</div>
);
}
function UserAvatar({ currentUser }: { currentUser: User }) {
return <img src={currentUser.avatarUrl} alt={currentUser.name} />;
}// Option 1: Composition — pass the component that needs the data
function Dashboard({ currentUser }: { currentUser: User }) {
return (
<div>
<Sidebar
userSection={
<UserSection>
<UserAvatar user={currentUser} />
</UserSection>
}
/>
<MainContent />
</div>
);
}
// Option 2: Context — for data needed across many components
const UserContext = createContext<User | null>(null);
function useCurrentUser() {
const user = useContext(UserContext);
if (!user) throw new Error("useCurrentUser must be used within UserProvider");
return user;
}
function Dashboard({ currentUser }: { currentUser: User }) {
return (
<UserContext.Provider value={currentUser}>
<Sidebar />
<MainContent />
</UserContext.Provider>
);
}
// UserAvatar consumes directly — no drilling
function UserAvatar() {
const user = useCurrentUser();
return <img src={user.avatarUrl} alt={user.name} />;
}During code review, trace any prop that appears in a component's signature but is never read within that component — only passed to a child. If this forwarding chain is 3+ components deep, it's prop drilling.
Look for components with many props they don't destructure or reference in their JSX.