← Back to Redux
Lesson 5 · State Management Fundamentals

Prop Drilling

Passing a value down through components that do not use it. When it is perfectly fine, what it really costs (measured - it is mostly not speed), and three ways out: composition, Context and a store.

Beginner35 min

What you will be able to do

  • Explain what prop drilling is, using a four-level example
  • Say when passing props down is fine - and why explicit props are often good
  • Name the real costs: changes touch every level, one typo breaks the chain, middle components are harder to reuse
  • Know from measurement that prop drilling is mostly not a speed problem
  • Remove drilling with composition (children and element props), without any new tool
  • Recognise when Context or a store becomes the better answer

The idea, in plain English

In React, data moves down from parent to child as props. When a component deep in the tree needs a value that lives near the top, the value has to travel through every component in between. If those middle components do not use the value - they only receive it and pass it on - that is called prop drilling.

Prop drilling is not a bug, and it is not always bad. Passing props is the normal, simple way React works, and for one or two levels it is the best choice: anyone reading the code can see exactly where the data comes from.

It becomes a problem when the chain gets long. Every middle component must mention the prop, so adding or renaming a value means editing many files, and one small mistake anywhere in the chain breaks it. Lesson 6 shows how state "climbs" up the tree; this lesson is about the long road back down.

Everything below was run with React 19.3 and Redux Toolkit 2.13.0, rendered in a test browser (jsdom). We counted how many times each component rendered, so the claims about speed are measured, not guessed.

Worked example: A user passed through four components that ignore it - Layout, Page, Sidebar - to reach the one that shows it.

workflowA value threaded through four componentsstep 1 / 4

1 - The value lives at the top

App holds the user in useState. App is the owner: it is the only component that can change the user.

owner
App
needs it
UserBadge
levels between
3
user
{ name: "Ravi" }

The example from the course outline: only UserBadge uses user, but Layout, Page and Sidebar must all pass it on.

ArchitectureThe same page with compositiontap a node to trace it

App creates UserBadge itself and hands the finished element down. Layout, Page and Sidebar never hear about user.

Example 1: the four-level chain

This is the example from the course outline. App owns the user. UserBadge, four levels down, shows the name. Layout, Page and Sidebar do not use user at all - but each one must accept it as a prop and pass it to its child, or UserBadge never gets it.

Read the comments: three of the five components exist in this chain only as messengers.

Drilling.jsx
import { useState } from "react"; function UserBadge({ user }) { // the ONLY component that uses user return <span>Hello, {user.name}</span>; } function Sidebar({ user }) { // does not use user - just passes it on return <aside><UserBadge user={user} /></aside>; } function Page({ user }) { // does not use user - just passes it on return <main><Sidebar user={user} /></main>; } function Layout({ user }) { // does not use user - just passes it on return <div><Page user={user} /></div>; } export default function App() { // owns the state const [user, setUser] = useState({ name: "Ravi" }); return <Layout user={user} />; }
Rendered HTML
<div><main><aside><span>Hello, Ravi</span></aside></main></div>

An everyday picture: passing a note along the row

In a classroom, the teacher wants to give a note to a student at the end of a long row. The teacher gives it to the first student, who passes it to the second, who passes it to the third, and so on. None of them needs the note. They only pass it on.

With two students in between, this works well - and everyone can see where the note came from. With six students in between, problems start. Every student must be present and paying attention. If one of them drops it, or passes it to the wrong person, the note never arrives - and the student at the end does not know which person in the row made the mistake. And if the teacher wants to send a second note, every student in the row is involved again.

The other option: the teacher walks over and puts the note directly on the right desk. That is what the fixes in this lesson do.

When passing props is fine

Do not be afraid of props. Passing a value one or two levels down - a TodoList giving onToggle to each TodoItem, a Form giving value and onChange to an Input - is not prop drilling in any worrying sense. It is just React.

Explicit props have a real advantage: the data flow is visible. You can read a component and see everything it depends on in its parameters. You can search for user= and find every place it is passed. Nothing is hidden. Many experienced React developers prefer a few levels of props over a more "clever" solution.

Prop drilling becomes a problem only when the chain is long, when many components in the chain ignore the value, or when the same value must be drilled to many places.

Fine or a problem?
Parent -> child, child uses itFine. This is normal React.
2 levels, the middle one also uses itFine. Nothing is wasted.
2 levels, the middle one ignores itUsually fine. Watch it.
3+ levels of components that ignore itProp drilling. Look at the fixes below.
The same value drilled to many branchesProp drilling - and a sign the value may be global (Lesson 2).

Cost 1: every change touches every level

Suppose UserBadge now also needs the theme, to choose its colours. With drilling, theme must be added to every component in the chain - not because they use it, but because they are on the road. We made that change: 5 components had to be edited (App, Layout, Page, Sidebar and UserBadge), 3 of them only to pass the value on.

With composition (shown below), the same change touched 2 components: App, which owns the theme, and UserBadge, which uses it. Both versions rendered exactly the same HTML.

DrillingTheme.jsx - adding theme to the chain
import { useState } from "react"; function UserBadge({ user, theme }) { return <span className={"badge " + theme}>Hello, {user.name}</span>; } function Sidebar({ user, theme }) { // changed: one more prop to pass return <aside><UserBadge user={user} theme={theme} /></aside>; } function Page({ user, theme }) { // changed: one more prop to pass return <main><Sidebar user={user} theme={theme} /></main>; } function Layout({ user, theme }) { // changed: one more prop to pass return <div><Page user={user} theme={theme} /></div>; } export default function App() { const [user] = useState({ name: "Ravi" }); const [theme] = useState("dark"); return <Layout user={user} theme={theme} />; }
Rendered HTML - the same for both versions
<div><main><aside><span class="badge dark">Hello, Ravi</span></aside></main></div>

Cost 2: one mistake breaks the chain

Every component in the chain is a place where the value can be lost. We changed one line in Sidebar from user={user} to usr={user} - a one-letter typo. UserBadge received no user, tried to read user.name, and the app crashed with TypeError: Cannot read properties of undefined (reading ‘name’).

Notice where the bug is: in Sidebar, a component that does not use user at all. When the error appears in UserBadge, the natural first step is to look at UserBadge - the wrong place. In a long chain you must walk up every level to find which one dropped the value. TypeScript catches this particular typo; it does not make the chain shorter.

One changed line in Sidebar
function Sidebar({ user }) { return <aside><UserBadge usr={user} /></aside>; // typo: usr } // Result when the app renders: // TypeError: Cannot read properties of undefined (reading 'name')

Cost 3: middle components are harder to reuse

Because Sidebar takes a user prop, every page that uses Sidebar must now have a user to give it - even a page where nobody is logged in, or where the sidebar shows something else. The component’s list of props describes the components below it, not what it does itself.

In a real app this grows: a Layout with ten props, eight of which it only passes on. Nobody dares to remove one, because they cannot easily see who below still needs it.

A common myth: "prop drilling makes the app slow"

You will often read that prop drilling causes extra re-renders. We measured it. We renamed the user and counted how many times each component rendered in five versions of the same page.

With drilling, all four components rendered once. But with composition, and even with React Context, all four also rendered once - because the user lives in App’s state, and when App re-renders, React re-renders its children by default. Context only reduced the renders when we also wrapped Layout in memo. Redux, which keeps the state outside the component tree, re-rendered only UserBadge.

So the honest summary is: prop drilling is mainly a cost in code - changes, mistakes, reuse - not in speed. And Context alone is not a speed fix. You will learn more about re-renders in Module 14 (Redux Performance).

Renders after renaming the user - measured
Prop drillingLayout 1, Page 1, Sidebar 1, UserBadge 1
CompositionLayout 1, Page 1, Sidebar 1, UserBadge 1
ContextLayout 1, Page 1, Sidebar 1, UserBadge 1
Context + memo(Layout)Layout 0, Page 0, Sidebar 0, UserBadge 1
Redux (useSelector)Layout 0, Page 0, Sidebar 0, UserBadge 1

Fix 1: composition - pass the finished piece down

The simplest fix needs no new tool. Instead of passing the data down and letting Sidebar create UserBadge, let App create UserBadge itself - App already has the user - and pass the finished element down. Layout and Sidebar take children; Page takes a sidebar prop. None of them knows that a user exists.

This is called composition, and the React documentation suggests trying it before reaching for Context. It works best when the middle components are "frames" - layouts, cards, panels - that place other components but do not need their data. The second diagram above shows its shape.

Composition.jsx
import { useState } from "react"; function UserBadge({ user }) { return <span>Hello, {user.name}</span>; } function Sidebar({ children }) { // a frame: shows whatever it is given return <aside>{children}</aside>; } function Page({ sidebar }) { // receives a finished element as a prop return <main>{sidebar}</main>; } function Layout({ children }) { return <div>{children}</div>; } export default function App() { const [user, setUser] = useState({ name: "Ravi" }); return ( <Layout> <Page sidebar={<Sidebar><UserBadge user={user} /></Sidebar>} /> </Layout> ); }

Fix 2: React Context

Sometimes composition does not fit: the component that needs the value is created deep inside other components, not by App. Then React Context lets a component high in the tree provide a value, and any component below read it directly with useContext - no props in between.

Context is convenient for values that many components read and that rarely change: the theme, the language, the logged-in user. Remember the measurement above, though: by itself it did not reduce re-renders. Lesson 7 compares Context and Redux in detail.

Context.jsx
import { createContext, useContext, useState } from "react"; const UserContext = createContext(null); function UserBadge() { const user = useContext(UserContext); // reads it directly return <span>Hello, {user.name}</span>; } function Sidebar() { return <aside><UserBadge /></aside>; } // no user prop function Page() { return <main><Sidebar /></main>; } // no user prop function Layout() { return <div><Page /></div>; } // no user prop export default function App() { const [user, setUser] = useState({ name: "Ravi" }); return ( <UserContext.Provider value={user}> <Layout /> </UserContext.Provider> ); }

Fix 3: a store

When the value is truly global - many components in many places read it and change it - a store like Redux removes the drilling and the owner component together. UserBadge selects the name from the store; nothing above it is involved. In our measurement, a rename re-rendered only UserBadge.

A store is the biggest step: more code and a new library. Use the order from Lesson 2 - props, then composition, then Context, then a store - and stop at the first one that solves your problem.

WithRedux.jsx
import { configureStore, createSlice } from "@reduxjs/toolkit"; import { Provider, useSelector } from "react-redux"; const userSlice = createSlice({ name: "user", initialState: { name: "Ravi" }, reducers: { renamed(state, action) { state.name = action.payload; }, }, }); const store = configureStore({ reducer: { user: userSlice.reducer } }); function UserBadge() { const name = useSelector((state) => state.user.name); // reads the store return <span>Hello, {name}</span>; } function Sidebar() { return <aside><UserBadge /></aside>; } function Page() { return <main><Sidebar /></main>; } function Layout() { return <div><Page /></div>; } export default function App() { return ( <Provider store={store}> <Layout /> </Provider> ); }

Where it stops being fine - a checklist

Use these signs to decide when to change approach. One sign on its own is not a reason to add a library; several together usually are.

Signs that drilling has gone too far
Three or more messengersThree or more components in a row receive a prop only to pass it on.
Long prop listsA component takes many props it never reads.
Renames are scaryRenaming one prop means editing many files.
Many branches need itThe same value is drilled down several different paths - it is probably global state.
Bugs in the wrong placeErrors appear in one component but the cause is in another one above it.

Common questions

Is prop drilling an anti-pattern? No. It is the normal way data moves in React. It becomes a problem only when the chain is long and full of components that ignore the value.

Does TypeScript solve prop drilling? It catches mistakes like the usr typo, which helps a lot. But every middle component still has to declare and pass the prop, so the cost of changes stays.

Should I use Context for everything to avoid props? No. Context hides where data comes from, and every consumer depends on the provider being above it. Use it for values that are needed widely, not to avoid writing two props.

What about spreading props, {...props}? Passing all props down with a spread makes the chain shorter to write, but hides which props each component really needs - the same problem, harder to see.

This lesson at a glance

Prop drilling

Passing a prop through components that only pass it on.

Props

Data from parent to child.

<UserBadge user={user} />
Composition

Pass a finished element instead of data.

<Sidebar><UserBadge user={user} /></Sidebar>
children

What a component receives between its tags.

function Sidebar({ children })
Context

Provide once, read anywhere below.

useContext(UserContext)
Store

Global state outside the tree.

useSelector((state) => state.user.name)

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Make the typo

“In Drilling.jsx, change user={user} to usr={user} in Sidebar. Read the error. Which component does it mention?”

Add a value

“Add a language prop that UserBadge shows. Count how many components you had to edit with drilling, then with composition.”

Count renders

“Add console.log("Layout render") to Layout in Context.jsx, then rename the user. Then wrap Layout in memo and try again.”

Find it in real code

“Open a React project you know. Find one prop that travels through three or more components. Could composition remove it?”

What usually goes wrong

Fearing all props

Passing a value one or two levels is normal React and keeps the data flow visible. Only long chains of messengers are a problem.

Reaching for Context first

Context hides the data flow and, by itself, did not reduce re-renders. Try composition first.

✗ <UserContext.Provider value={user}>   // for a value used once
✓ <Sidebar><UserBadge user={user} /></Sidebar>
Middle components that ask for data they do not use

A frame component should take children, not the data of the things inside it.

✗ function Sidebar({ user }) { return <aside><UserBadge user={user} /></aside>; }
✓ function Sidebar({ children }) { return <aside>{children}</aside>; }
Assuming drilling is the cause of slowness

We measured: drilling, composition and plain Context all re-rendered the same components. Measure before you change anything for speed.

Practice

Write these yourself before opening anything. Getting them wrong first is most of how this sticks.

1.

In Drilling.jsx, which components are only "messengers", and which component owns the user?

Show hint

A messenger receives user but never reads user.name.

Show solution
Answer
Owner: App (it has useState) Messengers: Layout, Page, Sidebar (they only pass user on) User: UserBadge (the only one that reads user.name)
2.

Rewrite this so that Card knows nothing about the user: function Card({ user }) { return <div className="card"><Avatar user={user} /></div>; }

Show hint

Let the parent create Avatar and give it to Card as children.

Show solution
Solution
function Card({ children }) { return <div className="card">{children}</div>; } // in the parent, which has the user: <Card> <Avatar user={user} /> </Card>
3.

For each case, choose props, composition, Context or a store: (a) a TodoItem needs onToggle from TodoList, (b) a Layout must show a UserBadge that App can build, (c) 40 components read the current language, (d) the cart is read and changed from many pages.

Show hint

Stop at the first option that solves the problem.

Show solution
Answer
(a) props - one level, the child uses it (b) composition - App builds UserBadge and passes it down (c) Context - read widely, changes rarely (d) a store - read and changed from many places
Coding challenge

Remove the drilling from a small shop

In this shop, the cart count is drilled through Header and Nav to reach CartIcon, and the add function is drilled through Shop and ProductList to reach each ProductCard. Use composition so that Header, Nav, Shop and ProductList take no cart props at all.

It should
  • Header, Nav, Shop and ProductList must not receive count or onAdd
  • Do not use Context or Redux - only children
  • Clicking "Add Python" then "Add Redux" must still show Cart (0) -> Cart (1) -> Cart (2)
Starter
import { useState } from "react"; function CartIcon({ count }) { return <span>Cart ({count})</span>; } function Nav({ count }) { // ignores count return <nav>Courses · Blog <CartIcon count={count} /></nav>; } function Header({ count }) { // ignores count return <header><Nav count={count} /></header>; } function ProductCard({ title, onAdd }) { return <button onClick={() => onAdd(title)}>Add {title}</button>; } function ProductList({ onAdd }) { // ignores onAdd return <section><ProductCard title="Python" onAdd={onAdd} /><ProductCard title="Redux" onAdd={onAdd} /></section>; } function Shop({ onAdd }) { // ignores onAdd return <main><ProductList onAdd={onAdd} /></main>; } export default function App() { const [cart, setCart] = useState([]); const add = (title) => setCart([...cart, title]); return ( <> <Header count={cart.length} /> <Shop onAdd={add} /> </> ); }
Show one solution
Solution - checked: Cart (0) -> Cart (1) -> Cart (2)
import { useState } from "react"; function CartIcon({ count }) { return <span>Cart ({count})</span>; } function Nav({ children }) { // knows nothing about the cart return <nav>Courses · Blog {children}</nav>; } function Header({ children }) { // knows nothing about the cart return <header>{children}</header>; } function ProductCard({ title, onAdd }) { return <button onClick={() => onAdd(title)}>Add {title}</button>; } function ProductList({ children }) { // knows nothing about the cart return <section>{children}</section>; } function Shop({ children }) { // knows nothing about the cart return <main>{children}</main>; } export default function App() { const [cart, setCart] = useState([]); const add = (title) => setCart([...cart, title]); return ( <> <Header> <Nav> <CartIcon count={cart.length} /> </Nav> </Header> <Shop> <ProductList> <ProductCard title="Python" onAdd={add} /> <ProductCard title="Redux" onAdd={add} /> </ProductList> </Shop> </> ); }

Key points

  • Prop drilling is passing a prop through components that only pass it on.
  • Passing props one or two levels is normal React, and keeps the data flow visible.
  • Costs grow with the chain: every change touches every level (5 components vs 2 in our theme example).
  • One typo in a middle component breaks the chain, and the error appears somewhere else.
  • Measured: drilling, composition and plain Context re-rendered the same components - drilling is mainly a code cost, not a speed cost.
  • First fix: composition - let the owner build the child and pass it down as children.
  • Then Context for widely read values, then a store for values read and changed everywhere.

Quick check before you move on

What is prop drilling?
Passing a prop down through components that do not use it, only so that a component further down can receive it.
Is passing a prop one level down prop drilling?
Not in any worrying sense. Parent to child is normal React.
Why was the typo in Sidebar hard to find?
The error appeared in UserBadge, but the mistake was in Sidebar - a component that does not even use user.
What is composition?
Building the child element in the component that has the data, and passing the finished element down as children or a prop.
Did Context alone reduce re-renders in our measurement?
No. All four components still re-rendered. Only Context plus memo, or Redux, re-rendered just UserBadge.

Interview questions

What is prop drilling, and is it bad?

Passing props through intermediate components that only forward them. It is not inherently bad - explicit props make data flow easy to trace - but long chains make changes expensive, couple unrelated components, and make bugs appear far from their cause.

How can you avoid prop drilling without Context or Redux?

With component composition: the component that owns the data renders the consumer and passes the element down through children or element props, so intermediate components never see the data.

Does Context solve re-render problems caused by prop drilling?

Not by itself. If the provider’s state lives in a parent component, updating it re-renders that subtree as usual. Context avoids passing props; reducing renders needs memoisation or a store with selector subscriptions.

When would you move from props to Context or a store?

When a value passes through several components that ignore it and composition does not fit, use Context for widely read, rarely changing values; use a store when many distant components read and change the value and changes need to be traceable.

Quiz

  1. 1.

    In Drilling.jsx, how many components pass user without using it?

  2. 2.

    Adding theme to UserBadge: how many components changed with drilling, and how many with composition?

  3. 3.

    True or false: prop drilling is the main reason React apps are slow.

  4. 4.

    Which fix does the React documentation suggest trying before Context?

  5. 5.

    Why does Sidebar({ children }) make Sidebar easier to reuse than Sidebar({ user })?

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...