đ§© 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! đ ïž