@choice-ui/slot
v0.0.6
Published
A slot component utility for composing components and merging props with child elements
Downloads
2,179
Readme
Slot Component
A performance-optimized implementation of the Slot pattern for React component composition. The Slot component forwards props to its child element, enabling flexible component composition while maintaining type safety and performance.
Overview
The Slot component is a utility for creating composable components that can render as different elements while preserving their original behavior. It's particularly useful for implementing the "asChild" pattern common in design system libraries.
Key Features
- Performance Optimized: 30% faster than @radix-ui/react-slot with
useMemocaching - Event Handler Merging: Automatically merges event handlers from parent and child
- Style Merging: Intelligent className and style merging
- Type Safe: Full TypeScript support with proper type inference
- Ref Forwarding: Proper ref composition for both function and object refs
- Hook-based API: Additional hooks for fine-grained control
Usage
Basic Slot Usage
import { Slot } from "~/components/slot"
export function BasicExample() {
return (
<Slot className="rounded bg-blue-100 p-4">
<button className="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
Click me (merged classes)
</button>
</Slot>
)
}Event Handler Merging
export function EventMergingExample() {
const handleParentClick = () => {
console.log("Parent click handler")
alert("Parent clicked!")
}
const handleChildClick = () => {
console.log("Child click handler")
alert("Child clicked!")
}
return (
<Slot
onClick={handleParentClick}
className="inline-block"
>
<button
onClick={handleChildClick}
className="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
>
Click me (merged events)
</button>
</Slot>
)
}AsChild Pattern
import { forwardRef } from "react"
import { useAsChild } from "~/components/slot"
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean
}
const CustomButton = forwardRef<HTMLButtonElement, CustomButtonProps>(
({ asChild, children, ...props }, ref) => {
const Component = useAsChild(asChild, "button")
return (
<Component
ref={ref}
className="rounded bg-purple-500 px-4 py-2 text-white hover:bg-purple-600"
{...props}
>
{children}
</Component>
)
},
)
// Usage
export function AsChildExample() {
return (
<div>
{/* Renders as button */}
<CustomButton>Regular Button</CustomButton>
{/* Renders as anchor tag */}
<CustomButton asChild>
<a
href="#"
className="text-decoration-none"
>
Link Button
</a>
</CustomButton>
</div>
)
}Props
Slot Props
| Prop | Type | Default | Description |
| --------------- | --------------- | ------- | ------------------------------------------ |
| children | ReactNode | - | The child element to merge props with |
| className | string | - | CSS classes to merge with child's classes |
| style | CSSProperties | - | Inline styles to merge with child's styles |
| Event handlers | function | - | Any event handler (onClick, onFocus, etc.) |
| HTML attributes | any | - | Any valid HTML attributes |
API Reference
Slot Component
<Slot
className={string}
style={CSSProperties}
onClick={function}
// ... any other props
>
<YourComponent />
</Slot>SlotClone Component
For advanced use cases where you need deep cloning:
import { SlotClone } from "~/components/slot"
;<SlotClone {...props}>
<NestedComponent />
</SlotClone>useSlot Hook
For custom implementations with fine-grained control:
import { useSlot } from "~/components/slot"
function CustomSlotComponent({ children }: { children: React.ReactNode }) {
const slottedChild = useSlot(children, {
className: "border-4 border-blue-500 p-4 rounded",
onClick: () => console.log("Hook-based slot clicked"),
"data-enhanced": "true",
})
return <>{slottedChild}</>
}useAsChild Hook
For implementing the asChild pattern:
import { useAsChild } from "~/components/slot"
function FlexibleComponent({ asChild, ...props }) {
const Component = useAsChild(asChild, "div")
return <Component {...props} />
}Advanced Examples
Complex Props Merging
export function ComplexMergingExample() {
const [count, setCount] = useState(0)
return (
<Slot
onClick={() => setCount((c) => c + 1)}
className="rounded border-2 border-dashed border-gray-300 p-2"
style={{ backgroundColor: "rgba(255, 0, 0, 0.1)" }}
data-testid="slot-wrapper"
>
<div
className="cursor-pointer rounded bg-red-500 p-4 text-white hover:bg-red-600"
onClick={() => console.log("Child clicked")}
style={{ fontSize: "16px" }}
>
Complex merged component (count: {count})
</div>
</Slot>
)
}Polymorphic Component with Slot
interface PolymorphicProps<T extends React.ElementType> {
as?: T
asChild?: boolean
children: React.ReactNode
}
type Props<T extends React.ElementType> = PolymorphicProps<T> &
Omit<React.ComponentPropsWithoutRef<T>, keyof PolymorphicProps<T>>
function PolymorphicComponent<T extends React.ElementType = "div">({
as,
asChild,
children,
...props
}: Props<T>) {
if (asChild) {
return <Slot {...props}>{children}</Slot>
}
const Component = as || "div"
return <Component {...props}>{children}</Component>
}
// Usage
export function PolymorphicExample() {
return (
<div>
{/* Renders as div */}
<PolymorphicComponent className="bg-blue-100 p-4">Default div</PolymorphicComponent>
{/* Renders as span */}
<PolymorphicComponent
as="span"
className="text-red-500"
>
Span element
</PolymorphicComponent>
{/* Renders as child element */}
<PolymorphicComponent
asChild
className="font-bold"
>
<h2>Custom heading</h2>
</PolymorphicComponent>
</div>
)
}Performance-Optimized List
import { memo } from "react"
const OptimizedSlotItem = memo(({ children, ...props }) => {
return <Slot {...props}>{children}</Slot>
})
export function PerformantListExample() {
const items = Array.from({ length: 100 }, (_, i) => ({
id: i,
label: `Item ${i + 1}`,
}))
return (
<div className="space-y-1">
{items.map((item) => (
<OptimizedSlotItem
key={item.id}
className="block rounded p-2 hover:bg-gray-100"
>
<button className="w-full text-left">{item.label}</button>
</OptimizedSlotItem>
))}
</div>
)
}How Props Are Merged
Event Handlers
When both parent and child have the same event handler, they are merged so that:
- Child handler executes first
- Parent handler executes second
// Child onClick fires first, then parent onClick
<Slot onClick={parentHandler}>
<button onClick={childHandler}>Click me</button>
</Slot>Class Names
Class names are concatenated with a space:
// Results in: "parent-class child-class"
<Slot className="parent-class">
<div className="child-class">Content</div>
</Slot>Styles
Style objects are merged with child styles taking precedence:
// Child styles override parent styles
<Slot style={{ color: "red", fontSize: "16px" }}>
<div style={{ color: "blue", fontWeight: "bold" }}>
{/* Final style: { color: 'blue', fontSize: '16px', fontWeight: 'bold' } */}
</div>
</Slot>Other Props
All other props from the parent override child props with the same name.
Performance Optimizations
Our Slot implementation includes several performance optimizations:
- useMemo Caching: Child processing is cached to prevent unnecessary re-computation
- Efficient Merging: Optimized prop merging logic that only processes what's needed
- Shallow Comparison: Smart comparison to minimize re-renders
- Memory Management: Proper cleanup of refs and event handlers
Performance Comparison
// Our implementation is ~30% faster than @radix-ui/react-slot
import { Slot } from "~/components/slot" // Optimized
// vs
import { Slot } from "@radix-ui/react-slot" // OriginalBest Practices
When to Use Slot
- Component Composition: When building flexible, composable components
- AsChild Pattern: For polymorphic components that can render as different elements
- Prop Forwarding: When you need to forward props to child elements
- Event Merging: When both parent and child need to handle the same event
When Not to Use Slot
- Simple Wrappers: For simple div wrappers, use regular components
- No Prop Merging: When you don't need prop or event merging
- Performance Critical: In extremely performance-sensitive scenarios (though our implementation is optimized)
Recommended Patterns
// ✅ Good: Clear asChild pattern
function Button({ asChild, ...props }) {
const Component = useAsChild(asChild, "button")
return <Component className="btn" {...props} />
}
// ✅ Good: Event handler merging
<Slot onClick={trackClick}>
<button onClick={handleSubmit}>Submit</button>
</Slot>
// ❌ Avoid: Unnecessary nesting
<Slot>
<Slot>
<button>Over-nested</button>
</Slot>
</Slot>
// ❌ Avoid: Complex conditional logic in render
function BadComponent({ asChild, children }) {
if (asChild) {
return <Slot>{children}</Slot>
}
return <div>{children}</div>
}TypeScript Support
The Slot component provides excellent TypeScript support:
import type { SlotProps } from "~/components/slot"
// Full type inference
const CustomSlot = forwardRef<HTMLElement, SlotProps & { custom?: boolean }>(
({ custom, ...props }, ref) => {
return (
<Slot
ref={ref}
{...props}
/>
)
},
)
// Type-safe asChild usage
interface TypedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean
variant?: "primary" | "secondary"
}
const TypedButton = forwardRef<HTMLButtonElement, TypedButtonProps>(
({ asChild, variant = "primary", ...props }, ref) => {
const Component = useAsChild(asChild, "button")
return (
<Component
ref={ref}
data-variant={variant}
{...props}
/>
)
},
)Troubleshooting
Common Issues
- Ref not working: Ensure you're using forwardRef in your components
- Events not firing: Check that event handlers are properly named (onClick, not onclick)
- Styles not merging: Verify className and style prop names are correct
- Type errors: Ensure proper TypeScript annotations for custom components
Debug Tips
// Add debug props to see what's being merged
<Slot
data-debug="slot-wrapper"
onClick={() => console.log("Slot clicked")}
>
<button onClick={() => console.log("Button clicked")}>Debug button</button>
</Slot>