General
Implementing Named Slots in React With Child Type Inspection
Kristijan Pajtasev DEV Community 周榜
4 views
React is deliberately minimal. It gives you components, props, and children while leaving structural composition patterns up to you. One pattern that doesn't exist natively but comes up constantly in design systems and component libraries is named slots: the ability to pass multiple distinct pieces of content into a parent component and have the parent decide where each one renders.
If you've used Vue.js you'll recognise this immediately. I am talking about what <slot name="..." /> does. React has no built-in equivalent, but you can implement the same thing cleanly using child type inspection. This article walks through exactly how.
The Goal
Say you're building a user profile card. Something of the kind you'd find on LinkedIn or a team directory. It has a fixed layout: a cover photo across the top, an avatar and name area, and a body divided into sections: personal details, work experience, and contact information. The consumer controls what goes into each section and whether optional ones appear at all.
The ideal call site looks like this:
<ProfileCard>
<ProfileHeader>
<Avatar src="/avatars/jane.jpg" />
<DisplayName>Jane Kowalski</DisplayName>
<Headline>Senior Product Designer · London</Headline>
</ProfileHeader>
<ProfileDetails>
<DetailItem label="Location">London, United Kingdom</DetailItem>
<DetailItem label="Industry">Technology</DetailItem>
<DetailItem label="Member since">March 2019</DetailItem>
</ProfileDetails>
<WorkExperience>
<ExperienceItem
company="Acme Corp"
role="Senior Product Designer"
period="2022 – present"
/>
<ExperienceItem
company="Bright Studio"
role="UX Designer"
period="2019 – 2022"
/>
</WorkExperience>
<ContactInfo>
<ContactItem type="email">jane@example.com</ContactItem>
<ContactItem type="linkedin">linkedin.com/in/janekowalski</ContactItem>
</ContactInfo>
</ProfileCard>
The JSX reads like a document. Each child self-describes its role. ProfileCard can render each section in the right place, conditionally wrap things, and add dividers without the consumer needing to know any of that. But how to handle inside of the <ProfileCard />what is defined and what is not.
The Core Mechanism
The trick is React.Children.toArray paired with a type comparison:
import React from "react";
import ProfileHeader from "./ProfileHeader";
import ProfileDetails from "./ProfileDetails";
import WorkExperience from "./WorkExperience";
import ContactInfo from "./ContactInfo";
const ProfileCard = ({ children }: { children: React.ReactNode }) => {
const arr = React.Children.toArray(children);
const header = arr.find(
(child) => React.isValidElement(child) && child.type === ProfileHeader
);
const details = arr.find(
(child) => React.isValidElement(child) && child.type === ProfileDetails
);
const experience = arr.find(
(child) => React.isValidElement(child) && child.type === WorkExperience
);
const contact = arr.find(
(child) => React.isValidElement(child) && child.type === ContactInfo
);
return (
<div className="profile-card">
<div className="cover-photo" />
{header && <div className="profile-header">{header}</div>}
<div className="profile-body">
{details}
{experience && (
<>
<Divider />
{experience}
</>
)}
{contact && (
<>
<Divider />
{contact}
</>
)}
</div>
</div>
);
};
Why this works
JavaScript functions have reference identity. When you write child.type === WorkExperience, you're comparing the element's type to the imported function reference. If the consumer passed <WorkExperience>, that element's .type is literally the WorkExperience function and the comparison passes. No string matching, no magic attributes, no registration step.
React.Children.toArray normalises the children into a flat array and handles edge cases like fragments, nulls, and single children. React.isValidElement narrows the TypeScript type before you access .type, keeping the compiler happy.
Enforcing Required Slots
Because this is a runtime check, you can throw a meaningful error when a required slot is missing:
if (!header) {
throw new Error(
"ProfileCard requires a <ProfileHeader /> child. Check that you have included it."
);
}
This fails loudly during development rather than silently rendering broken UI. It's the React equivalent of Vue's slot validation.
Conditional Structure Belongs to the Parent
One of the most powerful aspects of this pattern is that the parent fully owns layout decisions. Consider the dividers between profile sections. With a prop-based API the consumer would have to manage this. With named slots, ProfileCard handles it internally, then a section either appears with its divider or not at all, and the consumer never thinks about it.
The same principle applies inside WorkExperience. It inspects its own children and adds separators between entries:
const WorkExperience = ({ children }: { children: React.ReactNode }) => {
const arr = React.Children.toArray(children);
const items = arr.filter(
(child) => React.isValidElement(child) && child.type === ExperienceItem
);
return (
<section className="work-experience">
<h2 className="section-title">Experience</h2>
{items.map((item, index) => (
<React.Fragment key={index}>
{index > 0 && <Divider subtle />}
{item}
</React.Fragment>
))}
</section>
);
};
The developer omits an <ExperienceItem> and its separator simply doesn't render, there is no conditional logic outside of it. The same principle applies to spacing, borders, wrappers, ARIA roles, or any other structural detail the component needs to manage.
Order Independence
Unlike plain children rendering, the order the developer writes the slots doesn't matter. ProfileCard will always render header before details before experience before contact regardless of how the developer wrote them. This is a meaningful ergonomic improvement because developers focus on what they're providing, not where it will end up.
Nesting Slots
The pattern composes cleanly because each component is responsible only for its own children. ProfileCard inspects its direct children. WorkExperience inspects its own children independently. Neither knows or cares about the other's internals.
This means you get a natural component hierarchy that mirrors the UI structure:
<ProfileCard> {/* manages cover, header area, body dividers */}
<ProfileHeader> {/* manages avatar, name, headline layout */}
<Avatar src="..." />
<DisplayName>Jane Kowalski</DisplayName>
<Headline>Senior Product Designer</Headline>
</ProfileHeader>
<ProfileDetails> {/* manages label/value grid layout */}
<DetailItem label="Location">London</DetailItem>
<DetailItem label="Industry">Technology</DetailItem>
</ProfileDetails>
<WorkExperience> {/* manages entry separators */}
<ExperienceItem company="Acme Corp" role="Senior Designer" period="2022 – present" />
<ExperienceItem company="Bright Studio" role="UX Designer" period="2019 – 2022" />
</WorkExperience>
<ContactInfo> {/* manages icon rendering per contact type */}
<ContactItem type="email">jane@example.com</ContactItem>
</ContactInfo>
</ProfileCard>
Alternative Approaches
Render props
<ProfileCard
header={<ProfileHeader />}
details={<ProfileDetails />}
experience={<WorkExperience />}
contact={<ContactInfo />}
/>
Explicit and type-safe via the prop interface. Works well when the number of section is small and fixed. Gets complex with sections, and loses the document-like readability of the JSX tree.
Context API
You could wrap children in a Context provider and have each slot component read from it, this can be useful when slots need to react to shared runtime state (selected, expanded, loading). However, any component that calls useContext must be a Client Component in Next.js App Router. Forcing your slot components to be client components just to receive structural configuration adds bundle weight, introduces a hydration boundary, and prevents those components from accessing server-side data directly. If the slot components are purely presentational, child type inspection has none of these costs and works identically on server and client.
React.cloneElement
Previously common for injecting props into children after the fact. This is now considered legacy, it doesn't compose well with memoization, and the React team discourages it. Avoid.
When to Reach for This Pattern
This approach earns its complexity when:
A component has three or more named content regions with independent layout logic
Conditional layout (dividers, wrappers, spacing) depends on which slots are present
You want developers to write readable JSX rather than pass render functions or config objects
Slot components are server-safe. That means no hooks, no browser APIs
For simpler cases with one or two optional regions, named props with React.ReactNode type are usually sufficient and could be easier to follow.
Conclusion
Child type inspection gives React components a slot system that is order-independent, composable, and server-safe, and it is built entirely from primitives React already provides. This pattern puts layout decisions where they belong (inside the component) while keeping content decisions where they belong (with the developer). For complex UI building blocks like profile cards, data panels, complex lists and dashboard widgets, it's one of the cleanest structural patterns available in the React ecosystem.
For more, you can follow me on LinkedIn, GitHub, or Instagram.
Read original: https://dev.to/hi_iam_chris/implementing-named-slots-in-react-with-child-type-inspection-oh4
← Previous
Top 5 AI Governance Tools for Enterprises (2026)
Next →
New Warnings About the Risks of AI to Humanity Revive a Long-Running Debate
Related
What "Fully Automated" Actually Costs
General
0
DEV Community
Ethernet Speed Evolution Reaches 1.6 Terabit Milestone
General
1
DEV Community 周榜
I wrote three rules on Saturday. I broke all three on Saturday.
General
0
DEV Community 周榜
How I Built Calculadora SIU CrediUPE: Automating Credit Calculation in SIU Guaraní
General
1
DEV Community 周榜
Comments0
No comments yet — be the first