Topic 9.1
The Prop Drilling Problem
In one line
Prop drilling is passing a prop through components that don't use it, only so a component deep below can read it; it's fine for two or three levels, and when it gets painful the first fix is composition: pass finished JSX down as children instead of the raw data.
Think of it like this
A note passed along a row of students in class. Asha writes a note for Meera, who sits five seats away. Every student in between has to take the note and hand it on, even though it isn't for them. If one of them forgets, the note never arrives. It would be easier if Asha could put the note on a board that Meera can read directly, or walk it over herself.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Props
- The inputs a parent passes to a child component, like arguments to a function.
- Prop drilling
- Passing a prop through components that don't use it, only to reach one deep below.
- Middle component
- A component between the data's owner and the component that reads the data.
- Composition
- Building UI by putting components inside each other, often by passing JSX as
children. - children
- The special prop that holds whatever JSX you write between a component's opening and closing tags.
- Slot
- A prop that holds a piece of JSX to place somewhere specific, like
sidebar={<Menu />}. - Owner
- The component that holds a piece of state and passes it down.
Step by step
01See the chain
App owns user. Only UserMenu, three levels down, displays it. Page and Sidebar must accept user and pass it on, although they never read it.
02Ask: is it actually a problem?
Two levels of passing is fine. Count the levels, count the props, and check whether the middle components use them. If the middle components are small and the chain is short, leave it: explicit props are easy to follow and to test.
03Move the JSX up, not the data down
The owner of user is App. Let App build <UserMenu user={user} /> itself and hand the finished element down as a slot. Page and Sidebar just place what they're given.
function App() {
const [user, setUser] = useState({ name: "Asha" });
return (
<Page sidebar={<Sidebar><UserMenu user={user} /></Sidebar>}>
<Lesson />
</Page>
);
}
function Page({ sidebar, children }) {
return <main>{sidebar}{children}</main>; // knows nothing about user
}04The tree after composition
UserMenu still renders inside Sidebar inside Page on screen. But the data goes straight from App to UserMenu, because App is the component that wrote <UserMenu user={user} />.
05Know when composition is not enough
If the deep component must sit inside a component that you can't restructure (a library, a router layout), or if dozens of components all over the app need the same value, slots get awkward. That's the job of context, the next topic.
Try it yourself
- 1
Feel the cost of drilling
In the drilling example, make
UserMenualso show arole("Student"). Count how many components you must edit to pass it down. Then do the same in the composition example. (Drilling: three. Composition: one,App.) - 2
Add a second slot
In the composition example, give
Pageaheaderslot and pass<h1>Hello, {user.name}</h1>fromApp. Predict: doesPageneed to know aboutuser? (No. It renders{header}and that's all.) - 3
Break the bonus on purpose
In the bonus example, move
<LessonList />insideCard(render it directly instead of{children}). Predict the logs after the click, then check. (LessonList renderednow appears after every click, becauseCardcreates the element itself on each render.)
Code & diagrams
Three components touch `user`; only one uses it. Add a `theme` prop to `UserMenu` and you'd have to edit `Page` and `Sidebar` too.
Preview
Console
Nothing logged yet.Expected console output
Page got user only to pass it on
Sidebar got user only to pass it on
UserMenu uses user: AshaSame screen, but `Page` and `Sidebar` no longer mention `user`. They are general layout pieces you could reuse anywhere.
Preview
Console
Nothing logged yet.Expected console output
Page renders its slots, knows nothing about user
Sidebar renders its children
UserMenu uses user: AshaClicking changes `Card`'s own state, so `Card` renders again. `LessonList` doesn't: the element in `children` was made by `App`, which didn't render, so React sees the same element and skips it.
Preview
Console
Nothing logged yet.Expected console output
Card rendered, highlight = false
LessonList rendered
--- click Highlight
Card rendered, highlight = trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
One middle component forgets to pass the prop
In the drilling example, change Sidebar to render <UserMenu /> without user={user}.
Myth vs fact
Myth
Prop drilling is an anti-pattern and should always be replaced with context.
Fact
Passing props a few levels is normal and keeps data flow visible. Replace it only when the chain is long, the middle components don't use the props, and changes keep rippling through files.
Myth
Context is the first fix for drilling.
Fact
Composition usually comes first. Passing finished JSX as children or slot props removes the chain without adding a hidden dependency.
Myth
Components that receive children re-render the children every time they render.
Fact
The children elements were created by the parent. If only the wrapper's own state changed, they are the same objects as before and React skips them.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Slot props make inversion of control possible: the owner decides what goes inside, the layout decides where. That's why design-system components take
header,footer,iconandchildrenrather than a dozen data props. - ▸
The "children don't re-render" behaviour comes from React comparing element objects: when a parent re-renders, React compares each child element with the previous one, and if it's the same object (not just equal props), React skips that subtree. It's the same reason the React Compiler (Topic 16.5) can skip work by caching JSX.
- ▸
Composition costs a little readability: the place where a component is built (
App) is far from where it appears (insideSidebar). Name slots after where they go (sidebar,actions) so readers can follow the layout. - ▸
TypeScript makes drilling cheaper: renaming a prop with your editor's rename updates every link of the chain and errors where you forgot one (Phase 12). Many teams drill deeper than you'd expect for that reason.
Remember this
- 1
Props are the inputs a parent passes to a child (Topic 2.3). Data in React flows down through props, one level at a time (Topic 6.5). Prop drilling is what happens when a value is needed deep in the tree: every component between the owner of the data and the component that reads it must accept the prop and pass it on, even if it never uses it. The components in the middle are like the students handing on the note.
- 2
Drilling is not a bug, and for two or three levels it is often the best choice. The data flow is explicit: you can read a component's props and see exactly what it depends on, you can search the code for the prop name, and the component stays easy to reuse and test because everything it needs comes in through props. Many experienced developers drill on purpose before reaching for anything else.
- 3
Drilling becomes a problem when four things pile up: the chain is long (five, eight, ten levels); the middle components don't use the prop at all; there are many such props (
user,theme,locale,onLogout…); and the data's shape changes, so renaming or adding a field means editing every file on the path. The middle components also become harder to reuse, because they now require props that have nothing to do with their own job. - 4
Composition is the first fix to try, and it needs no new API. Instead of passing raw data down so a deep child can build its UI, the component that owns the data builds that piece of UI itself and passes the finished JSX down as
children(or another prop, often called a slot:sidebar={...},header={...}). The middle components just render{children}where it belongs. They no longer know the data exists, so there is nothing to drill (Topic 2.5 introducedchildren). - 5
Composition has a second benefit. JSX you receive as
childrenwas created by the parent that rendered you, not by you. When a layout component re-renders because of its own state, thechildrenit received are the same element objects as before, so React can skip rendering them again. Wrapping slow parts aschildrenof a stateful component is a well-known performance trick (Topic 16.1 explains the rules). - 6
When composition can't help, because many unrelated components scattered across the tree need the same value (the signed-in user, the colour theme, the language), React gives you context (Topic 9.2). A good order of choices: pass props; if the chain hurts, try composition; if many distant components need the value, use context; for large, fast-changing app-wide state, use a reducer plus context (Topic 9.6) or a store library (Phase 15).
Explain it without notes
What is prop drilling, and when is it not a problem?
How does composition with children remove prop drilling? Give an example.
Why doesn't a component passed as children re-render when the wrapper's own state changes?
What order of solutions would you try for sharing data deep in a tree?
Practice
A Dashboard passes currency through Panel and Row to Price. Neither Panel nor Row uses it. Rewrite it with composition so Panel and Row don't mention currency.
Write a Layout component with header, sidebar and children slots, and use it from App with a user that only the header shows.
Trade-offs
- ↔
Drilled props are explicit and easy to trace but noisy over long chains; composition removes the noise but separates where a component is built from where it appears.
- ↔
Composition keeps layout components generic and reusable; it can't help when a library or framework owns the layout and you can't pass slots through it. Then context fits better.
Done when you can
I can explain what prop drilling is and spot middle components that only pass props on.
I can decide whether a drilling chain is fine or worth fixing.
I can remove a drilling chain by passing finished JSX as
childrenor a slot prop.I can explain why
childrendon't re-render when the wrapper's own state changes.I know the order of fixes: props, composition, context, then a store.