React component composition is easier when you use slots. Learn how passing JSX as props helps you avoid prop drilling and simplifies your UI state logic.
I remember staring at a UserProfile component that had roughly 15 props passed down from a parent three levels deep. It was a nightmare to maintain, and every time we needed to add a small UI tweak, we had to touch three files just to pass a boolean flag through the middle.
If you’ve ever felt like your components are becoming "prop-drilling" machines, you’re not alone. We’ve all been there, trying to wrestle with complex layouts. Today, I want to talk about how to solve this using React component composition through the slot pattern.
When we first start with React, we learn that data flows down. Naturally, we pass data as props. But when you pass components or complex configurations through several layers, you’re creating a tight coupling between the parent and the child.
We once tried to solve this by creating a "God component" that handled everything. It broke because the state logic became impossible to trace, and re-renders were happening in parts of the UI that shouldn't have been involved. It took us about two days to realize that we were fighting the framework, not using it.
Instead of passing down configuration flags to control what a child renders, you should pass the rendered JSX itself. Think of your components as containers with empty "slots" waiting to be filled.
When you use props.children or named slots, you’re essentially saying: "This component manages the layout and the shell, but the parent decides what goes inside."
Look at this basic example of a Card component using the slot pattern:
JSX// The Container function Card({ header, children }) { return ( <div className="card"> <div className="card-header">{header}</div> <div className="card-body">{children}</div> </div> ); } // The Usage function App() { return ( <Card header={<h2>User Profile</h2>}> <ProfileDetails /> <ActionButtons /> </Card> ); }
By doing this, the Card doesn't need to know anything about ProfileDetails or ActionButtons. It doesn't care about their state, their props, or their logic. This is the heart of effective React patterns.
When you decouple the layout from the logic, your state management becomes much clearer. You stop worrying about where to store the "active" state or the "loading" state because those states usually belong to the components being passed into the slots, not the container itself.
If you’re struggling with where to put your data, revisit React props vs state: Mastering Unidirectional Data Flow to ensure you aren't lifting state higher than necessary. When you use slots, you keep your state local to the components that actually use it.
I’ve seen developers pass an object called config that contains 20 different keys just to render a button differently. This is exactly what React composition patterns: Escaping Props Hell with Slots warns against.
If you find yourself creating a massive configuration object, stop. Ask yourself: "Can I just pass a component here instead?"
By passing JSX as props, you gain a few immediate benefits:
While the slot pattern is powerful, don't over-engineer it. If your component is simple, a simple prop is fine. But once you notice that your component hierarchy is getting deep and you’re passing props that are only used by a grandchild, it’s time to refactor.
For those deep-dive scenarios, understanding React reconciliation and component state persistence: A mental model helps you realize that the way you structure your tree affects how React updates the DOM. Keeping your components composed cleanly makes this reconciliation process much more efficient.
Is passing JSX as a prop slow? In almost every real-world scenario, no. React is highly optimized. The overhead of passing a React element is negligible compared to the maintainability gains you get.
Should I use props.children or named slots?
Use props.children for the primary content. Use named slots (e.g., header, footer) when you have multiple distinct areas that need to be filled. It keeps the API of your component clear and predictable.
Does this make testing harder?
Actually, it makes testing easier! You can unit test your Card component in isolation without needing to mock the complex components that go into the slots.
I’m still experimenting with how to handle slot-based styling in complex design systems, but for 90% of the UI work I do, this approach has saved me from countless hours of debugging. Next time you feel the urge to add another prop, try creating a slot instead.
Master React purity to build predictable UI. Learn how functional programming in React keeps your components bug-free and easy to maintain as your app grows.