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.
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.
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.
The example from the course outline: only UserBadge uses user, but Layout, Page and Sidebar must all pass it on.
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.
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} />;
}<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.
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.
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} />;
}<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.
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).
Prop drillingLayout 1, Page 1, Sidebar 1, UserBadge 1CompositionLayout 1, Page 1, Sidebar 1, UserBadge 1ContextLayout 1, Page 1, Sidebar 1, UserBadge 1Context + memo(Layout)Layout 0, Page 0, Sidebar 0, UserBadge 1Redux (useSelector)Layout 0, Page 0, Sidebar 0, UserBadge 1Fix 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.
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.
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.
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.
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 drillingPassing a prop through components that only pass it on.
PropsData from parent to child.
<UserBadge user={user} />CompositionPass a finished element instead of data.
<Sidebar><UserBadge user={user} /></Sidebar>childrenWhat a component receives between its tags.
function Sidebar({ children })ContextProvide once, read anywhere below.
useContext(UserContext)
StoreGlobal 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.
“In Drilling.jsx, change user={user} to usr={user} in Sidebar. Read the error. Which component does it mention?”
“Add a language prop that UserBadge shows. Count how many components you had to edit with drilling, then with composition.”
“Add console.log("Layout render") to Layout in Context.jsx, then rename the user. Then wrap Layout in memo and try again.”
“Open a React project you know. Find one prop that travels through three or more components. Could composition remove it?”
What usually goes wrong
Passing a value one or two levels is normal React and keeps the data flow visible. Only long chains of messengers are a problem.
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>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>; }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.
In Drilling.jsx, which components are only "messengers", and which component owns the user?
Show hintHide hint
A messenger receives user but never reads user.name.
Show solutionHide solution
Owner: App (it has useState)
Messengers: Layout, Page, Sidebar (they only pass user on)
User: UserBadge (the only one that reads user.name)Rewrite this so that Card knows nothing about the user: function Card({ user }) { return <div className="card"><Avatar user={user} /></div>; }
Show hintHide hint
Let the parent create Avatar and give it to Card as children.
Show solutionHide solution
function Card({ children }) {
return <div className="card">{children}</div>;
}
// in the parent, which has the user:
<Card>
<Avatar user={user} />
</Card>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 hintHide hint
Stop at the first option that solves the problem.
Show solutionHide solution
(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 placesRemove 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.
- 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)
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 solutionHide solution
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
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.
In Drilling.jsx, how many components pass user without using it?
- 2.
Adding theme to UserBadge: how many components changed with drilling, and how many with composition?
- 3.
True or false: prop drilling is the main reason React apps are slow.
- 4.
Which fix does the React documentation suggest trying before Context?
- 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...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned