🧩 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! 🛠️