š§© Composition and Layout Components in React: A Better Way to Share State Without Prop Drilling
If youāve ever built a React app with nested components, youāve probably experienced prop drilling ā the tedious process of passing data and functions down through layers of components that donāt even use them.
Before diving into a better pattern, if youāre not familiar with prop drilling, take a moment to read this blog post. It gives a great overview of why it can be such a pain.
Letās look at a quick example of prop drilling in action:
function App() {const [count, setCount] = useState(0)const increment = () => setCount(c => c + 1)return <Child count={count} increment={increment} />}function Child({ count, increment }: { count: number; increment: () => void }) {return (<div><strong>I am a child and I don't actually use count or increment. My child doesthough, so I have to accept those as props and forward them along.</strong><GrandChild count={count} onIncrementClick={increment} /></div>)}function GrandChild({count,onIncrementClick,}: {count: numberonIncrementClick: () => void}) {return (<div><small>I am a grandchild and I just pass things off to a button</small><button onClick={onIncrementClick}>{count}</button></div>)}
Notice how Child
is forced to accept count
and increment
, even though it doesnāt use them? This adds coupling and noise, especially as your component tree grows.
Many developers turn to global state solutions or React Context to avoid this, but what if thereās a simpler way?
Instead of threading state through every layer, what if we just passed React elements instead of data?
Hereās the same functionality rewritten using the Composition and Layout Components Pattern:
function App() {const [count, setCount] = useState(0)const increment = () => setCount(c => c + 1)return (<ChildgrandChild={<GrandChildbutton={<button onClick={increment}>{count}</button>}/>}/>)}function Child({ grandChild }: { grandChild: React.ReactNode }) {return (<div><strong>I am a child and I don't actually use count or increment.</strong>{grandChild}</div>)}function GrandChild({ button }: { button: React.ReactNode }) {return (<div><small>I am a grandchild and I just pass things off to a button</small>{button}</div>)}
Now the App
component manages state and creates the actual UI elements. Child
and GrandChild
just arrange and display them. Much cleaner, right?
By passing elements instead of data, you:
Letās say you want to reuse the Child
component but change the GrandChild
layout ā you donāt need to change how props are passed or restructure your data. You just plug in a different element.
This composition technique is particularly helpful when:
ā ļø Of course, donāt go overboard. You can end up with overly abstract code if you pass every component as a prop. Use this pattern when it adds clarity, not confusion.
If youāre familiar with Vueās scoped slots, this pattern will feel familiar. In React, layout components are simply components that accept elements via props and render them in a specific structure.
The real magic comes from the fact that the parent owns the logic and the layout component owns the structure.
This pattern is not just theoretical. For example, kentcdodds.com uses this extensively ā particularly in the hero sections at the top of each page.
Kent also dives deeper into this idea in his blog post: One React mistake thatās slowing you down.
In your next project, try replacing some of your prop chains with this composition pattern. Youāll notice how much more scalable and maintainable your component tree becomes.
For a hands-on exercise, try refactoring a component where youāre passing multiple props down just for a button. Instead, pass the button itself as a React element. Watch how it simplifies everything.
Happy composing! š ļø
Quick Links
Legal Stuff
Social Media