Suggest an editImprove this articleRefine the answer for “What is prop drilling and how to avoid it”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Prop drilling** - passing props through intermediate components that don't use them, just to reach a deeply nested child. ```tsx function App() { return <Parent user={user} />; } function Parent({ user }) { return <Child user={user} />; } // Parent doesn't use user function Child({ user }) { return <p>{user.name}</p>; } ``` **Key point:** fix with Context API at 3+ levels, or a state manager like Zustand for app-wide state. Plain props are fine for 1-2 levels.Shown above the full answer for quick recall.Answer (EN)Image**Prop drilling** happens when data travels through component layers as props, even though intermediate components don't use it. ## Theory ### TL;DR - Analogy: you pass a note through five people to reach one person at the end. Everyone in the middle holds it but reads nothing. - Main problem: intermediate components get polluted with props they don't care about, making refactoring painful. - Decision rule: 1-2 layers deep is fine. At 3+ layers, Context or a state manager is worth it. - Context skips intermediates entirely. State managers (Redux, Zustand) go further for app-wide logic. - Drilling creates coupling. Adding a new prop means updating every component in the chain. ### Quick example ```tsx // Prop drilling: user tunnels through Parent which ignores it function App() { const user = { name: 'John' }; return <Parent user={user} />; // passes down } function Parent({ user }) { // receives, doesn't use return <Child user={user} />; // must forward anyway } function Child({ user }) { return <p>Hello, {user.name}!</p>; // finally uses it } ``` `Parent` has no reason to know about `user`. But it does, because `Child` needs it. That is prop drilling. ### Why it gets painful At 2 layers, no one cares. At 4-5 layers, renaming a prop means touching every component in the chain. Add a new feature flag that `GrandChild` needs, and now you update `App`, `Layout`, `Sidebar`, and `Panel` just to thread one boolean through. The real cost is coupling. Components that don't use a prop still depend on it structurally. Test one, and you have to mock props that component doesn't even care about. ### When to use what - 1-2 levels deep: keep props, explicit data flow is a feature here - Theme, locale, auth data across 3+ components: Context API - Complex state with async actions or persistence: Redux Toolkit or Zustand - Frequent updates across many consumers: state manager over Context, to avoid mass re-renders ### Comparison table | Aspect | Prop Drilling | React Context | Redux/Zustand | |--------|---------------|---------------|---------------| | Data flow | Manual at every level | Provider wraps subtree | Global store + selectors | | Re-renders | Only direct children | All consumers on value change | Controlled via `useSelector` | | Boilerplate | None initially, grows fast | Provider + `useContext` | Actions/reducers (simpler in RTK) | | Testing | Prop mocking straightforward | Context wrapper needed | Store isolation | | Best for | Flat trees (<3 levels) | UI state, medium apps | Large apps, async logic | ### How React handles this internally React builds a fiber tree top-down. Prop drilling adds unnecessary data to intermediate fiber nodes, and every level in the chain carries it through reconciliation. Context works differently: it uses a special `Context` fiber that caches the current value and notifies only consumers via a separate dispatcher queue. Components that don't call `useContext` skip the update entirely. ### Fixing drilling with Context ```tsx const CartContext = createContext(); function Dashboard() { const [cartTotal, setCartTotal] = useState(0); return ( <CartContext.Provider value={{ cartTotal, setCartTotal }}> <Header /> </CartContext.Provider> ); } function Header() { return <Sidebar />; // no cartTotal prop needed } function Sidebar() { return <CartWidget />; // no cartTotal prop needed } function CartWidget() { const { cartTotal } = useContext(CartContext); return <span>{cartTotal} items</span>; } ``` `Header` and `Sidebar` are clean. They don't know `cartTotal` exists. `CartWidget` pulls it directly. ### Common mistakes **Forgetting to forward a new prop during refactoring** ```tsx // Added newFeature to App, forgot the path to Child <Parent user={user} newFeature={true} /> function Parent({ user }) { return <Child user={user} />; // newFeature silently missing } ``` `Child` gets `undefined`. This is the classic refactoring bug with deep drilling. With Context, consumers declare what they need independently, so adding a value to the provider doesn't require touching intermediate components. **Putting everything in one top-level Context** ```tsx <UserContext.Provider value={user}> <EntireApp /> </UserContext.Provider> ``` Every `useContext(UserContext)` consumer re-renders when `user` changes. For an app with dozens of consumers, that adds up fast. Scope your providers closer to where data is actually needed. **No default value in createContext** ```tsx const Ctx = createContext(); // no default function Consumer() { const val = useContext(Ctx); // crashes outside Provider } ``` If `Consumer` ever renders outside a Provider (in tests, portals, or lazy-loaded routes) you get a runtime error. Fix: `createContext({})` or `createContext(null)` with a guard. **Object value without memoization** ```tsx function App() { const [theme, setTheme] = useState('light'); return ( // new object every render = all consumers update <ThemeContext.Provider value={{ theme, setTheme }}> <Button /> <UnrelatedChart /> {/* re-renders on every App render */} </ThemeContext.Provider> ); } ``` A new object reference on every render triggers all consumers. Wrap with `useMemo`: `const value = useMemo(() => ({ theme, setTheme }), [theme])`. In my experience, this is the one that shows up most in code reviews: someone replaced drilling with Context and wondered why performance got worse. ### Real-world usage - Chakra UI: `<ChakraProvider theme={customTheme}>` injects the theme globally so no component needs theme props drilled - Material-UI: `<ThemeProvider>` for styling across modals and drawers - Next.js app router: auth state lives in a Context Provider wrapping layout components - Large production apps: Redux for business state, Context for UI-layer theming and locale ### Follow-up questions **Q:** Show prop drilling in an App -> Layout -> Sidebar -> UserMenu tree, then fix it with Context. **A:** Pass `user` down through each layer as a prop. Then create `UserContext`, wrap `Layout` in `UserContext.Provider`, and call `useContext(UserContext)` directly in `UserMenu`. Layout and Sidebar become clean. **Q:** When does Context cause more re-renders than prop drilling? **A:** When the Provider value is a new object or array each render without `useMemo`. All consumers reconcile even if the data didn't logically change. Drilling only re-renders direct children. **Q:** Context vs Redux for a shopping cart? **A:** Context works for a simple cart total (local UI state). Redux is worth it once you need persistence, server sync, or complex actions like applying discount codes with async validation. **Q:** How do you test a component that depends on Context without drilling? **A:** Wrap the component in the Provider inside the test: `render(<CartContext.Provider value={mockCart}><CartWidget /></CartContext.Provider>)`. **Q:** In React 18+, how does concurrent mode affect drilled props vs Context? **A:** Drilling can block suspense boundaries because the data isn't available until the parent renders. Context with `useTransition` lets React mark updates as non-urgent, so the UI stays responsive during slow renders. ## Examples ### Basic: user greeting without and with Context ```tsx // Problem: user drilled through Layout that doesn't need it function App() { const user = { name: 'Alice', role: 'admin' }; return <Layout user={user} />; } function Layout({ user }) { // doesn't use user return <Navbar user={user} />; } function Navbar({ user }) { return <span>Welcome, {user.name}</span>; } // Fix: Context const UserContext = createContext(null); function App() { const user = { name: 'Alice', role: 'admin' }; return ( <UserContext.Provider value={user}> <Layout /> </UserContext.Provider> ); } function Layout() { return <Navbar />; // no user prop } function Navbar() { const user = useContext(UserContext); return <span>Welcome, {user.name}</span>; } ``` `Layout` is now completely decoupled from user data. Adding `role` or `email` to the user object doesn't require touching `Layout` at all. ### Intermediate: e-commerce dashboard with cart Context ```tsx const CartContext = createContext(); function Dashboard() { const [cartTotal, setCartTotal] = useState(3); return ( <CartContext.Provider value={{ cartTotal, setCartTotal }}> <Header /> </CartContext.Provider> ); } function Header() { return ( <nav> <Logo /> <Sidebar /> </nav> ); } function Sidebar() { return <CartWidget />; } function CartWidget() { const { cartTotal } = useContext(CartContext); return <button>Cart ({cartTotal})</button>; } ``` Adding another cart consumer (like a checkout summary) means just calling `useContext(CartContext)` in that component. No prop changes needed anywhere in the tree. ### Advanced: re-render trap and the fix ```tsx // Triggers re-renders in ALL consumers on every App render const ThemeContext = createContext(); function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {/* new object every render */} <Button /> <ExpensiveChart /> {/* re-renders even on unrelated state changes */} </ThemeContext.Provider> ); } // Fix: memoize the value function App() { const [theme, setTheme] = useState('light'); const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> <Button /> <ExpensiveChart /> {/* now only re-renders when theme changes */} </ThemeContext.Provider> ); } ``` `setTheme` is stable (same reference from `useState`), so `useMemo` with `[theme]` only produces a new object when theme actually changes. `ExpensiveChart` stops updating on unrelated renders.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.