Component Composition: why 'slot' patterns beat prop drilling every time
I spent way too long drilling props three levels deep before I learned that composition fixes most of these problems instantly. Once you see it, you can't unsee it.
The prop drilling trap
You start with a button. Then a modal wraps the button. Then a form wraps the modal. Then the page wraps everything. By the time you're styling that button, you're threading size, variant, disabled, onClick through six layers of components. Each layer adds indirection. Each indirection is a place where bugs hide.
// ❌ This scales poorly
<Page>
<Form buttonSize="lg" buttonVariant="primary">
<Modal title="Confirm" actionButtonLabel="Save">
<Button size={buttonSize} variant={buttonVariant} />
</Modal>
</Form>
</Page>Composition via children and slots
The fix is simple: stop passing data through the tree. Instead, pass the component itself.
// ✅ Compose directly
<Page>
<Form
actions={<Button size="lg" variant="primary">Save</Button>}
>
<Modal title="Confirm">
{/* Button is already baked in, no props needed */}
</Modal>
</Form>
</Page>Now the button is defined once, at the call site. Modal doesn't care about button props. Form doesn't care either. Each component only knows its own business.
Render props for dynamic control
Sometimes you need the child to control what renders. That's where render props shine:
<DataTable
data={items}
renderRow={(item) => (
<tr>
<td>{item.name}</td>
<td>{item.value}</td>
</tr>
)}
/>The table owns the iteration and row context. The caller owns the presentation. No prop drilling. The logic stays clean.
Real-world wins
I used this pattern recently on a filterable list component. Instead of passing onFilter, filterPlaceholder, filterDebounce, etc., I just accepted:
<FilterableList
data={items}
renderFilter={(value, onChange) => (
<input value={value} onChange={onChange} placeholder="Search..." />
)}
renderItem={(item) => <ItemCard {...item} />}
/>The component owns filtering logic. The caller owns UI. Zero prop drilling. Dead simple to test each piece independently.
When context still wins
For truly global state (theme, locale, user), context is still the right choice. But for local component communication? Composition beats context every time because it's explicit, debuggable, and doesn't hide dependencies.
Key takeaway
Prop drilling feels like it'll just be two more props. Then it's five. Then it's seventeen and you're wondering why a button component needs to know about form state.
Composition (slots, render props, or children) is not fancier — it's clearer. The data flows where it's visible. The component boundaries stay clean. Your future self will be grateful.
— Mustaque