In one minute
Prop drilling is passing a prop through components that don’t use it, so that a component further down can. It often happens with layout components: a Nav, a Main, a Footer whose only job is to decide where things go. The fix is to stop passing them data and pass them finished elements instead. The component that owns the data builds the element (<img src={user.avatar} />), and the layout component just places it. For one region that’s children; for several regions, named props like sidebar and content.
You’ll be able to: spot a layout component that’s forwarding data, and turn it into one that takes elements.
1function App() {2 const [user] = useState(sam)3 const [selected, setSelected] = useState(null)4 return (<>5 <Nav user={user} />6 <Main products={products} selected={selected}7 onSelect={setSelected} />8 <Footer user={user} />9 </>)10}1112function Nav({ user }) {13 return <nav><img src={user.avatar} /></nav>14}- render phase
const [user] = useState(sam)Appowns the user, the products and the selected product. - render phase
<Nav user={user} />Navonly places an avatar, yet it has to take the wholeuserto build it. - render phase
<Main products={products} selected={selected}Mainonly puts two areas side by side, yet it receives three props it just passes on. - render phase
<Footer user={user} />Same forFooter. Every layout component now depends on data it doesn’t use: changeUserand their types change too.
1function App() {2 const [user] = useState(sam)3 const [selected, setSelected] = useState(null)4 return (<>5 <Nav avatar={<img src={user.avatar} />} />6 <Main7 sidebar={<ProductList onSelect={setSelected} />}8 content={<ProductDetails product={selected} />} />9 <Footer>Happy shopping, {user.name}!</Footer>10 </>)11}1213function Nav({ avatar }) {14 return <nav>{avatar}</nav>15}- render phase
const [user] = useState(sam)The same state, in the same place. - render phase
<Nav avatar={<img src={user.avatar} />} />Appbuilds the avatar element itself, whereuserlives, and handsNavthe finished element. - render phase
sidebar={<ProductList onSelect={setSelected} />}Two areas, two named element props.Mainonly decides where they go. - render phase
<Footer>Happy shopping, {user.name}!</Footer>One area: plainchildren. Recorded: no layout component received any data.
The example: the product page
The page has a nav bar with the shopper’s avatar, a main area with a product list and the selected product’s details, and a footer greeting. App owns the user, the products and the selection.
1. Data drilled through the layout
function AppDrilled() {
const [user] = useState<User>({ name: 'Sam', avatar: '/logo.svg' })
const [selected, setSelected] = useState<Product | null>(null)
return (
<>
<NavDrilled user={user} />
<MainDrilled products={products} selected={selected} onSelect={setSelected} />
<FooterDrilled user={user} />
</>
)
}
function NavDrilled(p: { user: User }) {
props('Nav', p)
return <nav><img src={p.user.avatar} alt={`${p.user.name}'s profile`} /></nav>
}
function MainDrilled(p: { products: Product[]; selected: Product | null; onSelect: (p: Product) => void }) {
props('Main', p)
return (
<main>
<ProductList products={p.products} onSelect={p.onSelect} />
<ProductDetails product={p.selected} />
</main>
)
}
function FooterDrilled(p: { user: User }) {
props('Footer', p)
return <footer>Happy shopping, {p.user.name}!</footer>
}
What each layout component received:
Nav got: user
Main got: products, selected, onSelect
Footer got: user
Nav and Footer take the whole user to show an image and a name. Main takes three props it never reads, only passes on.
2. Elements built where the data is
function AppComposed() {
const [user] = useState<User>({ name: 'Sam', avatar: '/logo.svg' })
const [selected, setSelected] = useState<Product | null>(null)
return (
<>
<Nav avatar={<img src={user.avatar} alt={`${user.name}'s profile`} />} />
<Main
sidebar={<ProductList products={products} onSelect={setSelected} />}
content={<ProductDetails product={selected} />}
/>
<Footer>Happy shopping, {user.name}!</Footer>
</>
)
}
function Nav(p: { avatar: ReactNode }) {
props('Nav', p)
return <nav>{p.avatar}</nav>
}
function Main(p: { sidebar: ReactNode; content: ReactNode }) {
props('Main', p)
return (
<main>
{p.sidebar}
{p.content}
</main>
)
}
function Footer(p: { children: ReactNode }) {
props('Footer', p)
return <footer>{p.children}</footer>
}
Nav got: avatar
Main got: sidebar, content
Footer got: children
None of the layout components sees user, the products or setSelected any more. Both versions behave the same (clicking Desk Lamp showed Desk Lamp: $39.00 in each). What changed is the dependencies: if User gains a field, or the list needs a new prop, only App and the component that actually uses it change.
The components that really use the data, ProductList and ProductDetails, still take normal props. The pattern is for the components in between.
How it works
- A JSX element is just a value.
<img … />is an object you can store in a variable or pass as a prop (Using JSX). The layout component renders it with{avatar}, wherever it wants. childrenis the one-slot case.<Footer>Happy shopping</Footer>isFooter({ children: 'Happy shopping' }). Use named element props (sidebar,content) only when there’s more than one region.- State doesn’t move.
selectedstill lives inApp. Composition changes how the pieces that read it get to their place, not where it’s stored. - A performance bonus: an element passed in from above is the same object when only the layout component re-renders, so React can skip it. That’s covered in the Performance module.
Common mistakes
- Reaching for context first. Context is for values many unrelated components need (theme, user). If one piece of UI just needs to be placed somewhere, pass the element.
- Turning every prop into an element prop. A component that reads and branches on data (like
ProductDetailschecking whether a product is selected) should take the data. - Ten named slots. If a layout takes so many element props that it’s hard to read, look at Compound Components or The Slots Pattern.
Interview Q&A
What is prop drilling, and why is it a problem?
Passing props through components that don’t use them, just to reach a descendant. Every component in the chain then depends on data it doesn’t care about, so changes to that data ripple through all of them. Recorded: Main received products, selected, onSelect and used none of them.
How does composition fix it?
Build the element where the data lives and pass the element down. Layout components take ReactNode props (or children) and only decide where to render them. Recorded: Nav got avatar, Main got sidebar, content, Footer got children: no data at all.
When do you use children vs named element props?
children when there’s one region to fill. Named props like sidebar and content when a layout has several independent regions.
Isn't this just context with extra steps?
No. Context shares a value with many components implicitly. Composition places a specific piece of UI. Reach for composition first: it adds no provider, no hook, and no extra re-renders.
When does it stop paying off?
When a layout component’s list of element props gets long and hard to follow. Then the pieces often belong together as compound components or slots.
Related
- Custom Components: props and
children. - Compound Components and The Slots Pattern: composition with implicit wiring.
- Context with use: the alternative for widely shared values.
Sources
- Kent C. Dodds: Prop drilling, One React mistake that’s slowing you down
- react.dev: Passing JSX as children
- Topic order inspired by Kent C. Dodds’ EpicReact Advanced React Patterns workshop; the example app and code here are this handbook’s own.
Primary source: kentcdodds.com/blog/prop-drilling