Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~23 min· topic 1 of 6

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.

See the chaindiagram
Rendering diagram…

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.

App.jsxwhole filejsx
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} />.

The tree after compositiondiagram
Rendering diagram…

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. 1

    Feel the cost of drilling

    In the drilling example, make UserMenu also show a role ("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. 2

    Add a second slot

    In the composition example, give Page a header slot and pass <h1>Hello, {user.name}</h1> from App. Predict: does Page need to know about user? (No. It renders {header} and that's all.)

  3. 3

    Break the bonus on purpose

    In the bonus example, move <LessonList /> inside Card (render it directly instead of {children}). Predict the logs after the click, then check. (LessonList rendered now appears after every click, because Card creates the element itself on each render.)

Code & diagrams

Prop drilling: the user passes through two components that don't use it New tab

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

Press Run to see the page.

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: Asha
Composition: the owner builds the piece and passes it down as a slot New tab

Same screen, but `Page` and `Sidebar` no longer mention `user`. They are general layout pieces you could reuse anywhere.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Page renders its slots, knows nothing about user
Sidebar renders its children
UserMenu uses user: Asha
Bonus: children don't re-render when the wrapper's own state changes New tab

Clicking 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Card rendered, highlight = false
LessonList rendered
--- click Highlight
Card rendered, highlight = true

Break 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}.

terminal
$ Console
── what you'll see ──
Page got user only to pass it on
Sidebar got user only to pass it on
Page got user only to pass it on
Sidebar got user only to pass it on
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
An error occurred in the <UserMenu> component.
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

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, icon and children rather 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 (inside Sidebar). 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. 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. 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. 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. 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 introduced children).

  5. 5

    Composition has a second benefit. JSX you receive as children was created by the parent that rendered you, not by you. When a layout component re-renders because of its own state, the children it received are the same element objects as before, so React can skip rendering them again. Wrapping slow parts as children of a stateful component is a well-known performance trick (Topic 16.1 explains the rules).

  6. 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

01

What is prop drilling, and when is it not a problem?

02

How does composition with children remove prop drilling? Give an example.

03

Why doesn't a component passed as children re-render when the wrapper's own state changes?

04

What order of solutions would you try for sharing data deep in a tree?

Practice

01

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.

02

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 children or a slot prop.

  • I can explain why children don't re-render when the wrapper's own state changes.

  • I know the order of fixes: props, composition, context, then a store.