Renderable and renderSlot is a small utility for React that makes it easier to define slots in your components, handle default parts, support render props, and wrap content consistently.
It merges multiple patterns:
- Render props
- Slots
- Template method
- Default content fallback
- Conditional rendering
- Portals
This enables you to build components with flexible APIs without complicated prop juggling.
npm install render-slot(or copy the renderSlot.ts, Renderable.ts and useGateway.ts files into your utils folder)
renderSlot({
// The custom rendering input (ReactNode, Function, Object Props, True/False, Array, Portal etc.)
bespoke?: Renderable<Props, Context>,
// The default slot implementation
default?: ReactNode | ComponentType<Props>,
// Additional context passed to custom render function
context?: Context,
// Optional wrapper applied to final output (or every element of an array)
wrapper?: (part: ReactNode, index?: number) => JSX.Element,
// Auto-wrap primitive bespoke values into default
options?: {
wrapNonElementWithDefault?: boolean
}
}): ReactNodeOverloaded signatures:
renderSlot({ bespoke?, default?, context?, wrapper?, options? })
renderSlot(bespoke, { default?, context?, wrapper?, options? })
renderSlot(bespoke, default, { context?, wrapper?, options? })
renderSlot(bespoke, default, context, { wrapper?, options? })
renderSlot(bespoke, default, context, wrapper, options?)Library provides all possible overrides to match your style of programming.
The bespoke slot property (what is passed to component that uses renderSlot) can be many different things:
export type Renderable<P = Record<string, any>, C extends Props = Props> =
| ReactNode
// custom implementation or condition e.g. <span>Hello</span>, true, null
| Partial<P>
// props for default implementation e.g { propA: 10, propB: 'string' }
| ((Default: ComponentType<P>, context: C) => ReactNode)
// render prop with default and context data e.g. (Default, context) => <Default propA={context.loading} />
| (() => ReactNode)
// render prop without default e.g. () => <div>Text</div>
| Renderable<P, C>[]
// Many alterntives sequentially e.g. [{ propA: 10, propB: 'string' }, <div>Text</div>]function Component({ renderText }: { renderText: Renderable }) {
return renderSlot(renderText, (props) => <span {...props}>Default text</span>);
}
function Component({ renderText }: { renderText: Renderable }) {
return renderSlot(renderText, <span>Default text</span>);
}<Component renderText />
// ✅ Renders default part
<Component renderText={false} />
// ✅ Renders nothing<Component renderText={null} />
// ✅ Renders nothing
<Component renderText={undefined} />
// ✅ Renders nothing
<Component renderText={[]} />
// ✅ Renders nothing<Component renderText />
// → <span>Default text</span>
<Component renderText={{}} />
// → <span>Default text</span>
<Component renderText={{ color: 'red' }} />
// → <span color="red">Default text</span>
Here, instead of JSX, you pass an **object of props**, and it’s merged into the default.<Component renderText={<strong>Custom text</strong>} />
// → <strong>Custom text</strong><Component renderText={(Default, context) => <strong>{context.someData ? <Default /> : null}</strong>} />
// → <strong><span>Default text</span></strong>Here Default is passed into your function, so you can wrap or extend the default. Variable context on the
other hand, represents every additional data that is passed to custom renderer by the child component.
renderSlot(
true,
() => <span>Default text</span>,
{ isLoading: true },
(part) => <li>{part}</li>
)
// → <li><span>Default text</span></li>Wrappers are great for list items, tooltips, or other consistent containers.
renderSlot("Hello World", (props) => <span {...props} />, undefined, { wrapNonElementWithDefault: true })
// → <span>Hello World</span>function Component({ renderText }: { renderText?: Renderable<{ propA: number }> }) {
return renderSlot(
renderText,
({ propA }) => <div>Example:{propA}</div>,
)
}
function Client() {
return (
<div className="Client component">
<div id="slot" />
<Component
renderText={(Text) => {
return createPortal(
<Text propA={8} />,
document.querySelector('#slot')
)
}}
/>
</div>
)
}
// → <div className="Client component"><div id="slot"><div>Example:8</div></div></div>
function Client() {
const [Text, renderText] = useGateway<typeof Component, 'renderText'>()
return (
<div className="Client component">
<Text propA={8} />
<Component renderText={renderText} />
</div>
)
}
// → <div className="Client component"><div>Example:8</div></div>renderSlot(["Hello World", { propA: 2 }, <span>CutomText</span>])
// → Hello World<div>Example:2</div><span>CutomText</span>| Code | Output |
|---|---|
<MyComp renderText /> |
![]() |
<MyComp renderText={<strong>Bold!</strong>} /> |
![]() |
<MyComp renderText={(Default) => <em><Default /></em>} /> |
![]() |
The logic for renderSlot works like this:
false | null | undefined→ renders nothingtrue→ renders defaultfunction→ renders function(default, context)props object→ render default(props)- primitive (string/number) with
wrapPrimitiveWithDefault→ render default({children}) - otherwise → render as-is
- In case array is provided start from point 1 for every element of the array
- Use
renderSlotin component APIs to make them flexible without dozens of props. - Provide sensible defaults (don’t force users to always override).
- Combine with
wrapperfor lists, layouts, and consistent styles. - Use
wrapPrimitiveWithDefaultto accept strings/numbers in text slots. - Use Portal within render function or useGateway hook if you want to render component's slot outside of it.
type CardProps = {
renderHeader?: Renderable<{ className: string }, { isLoading: boolean }>;
renderFooter?: Renderable<{ className: string }>;
};
function Card({ renderHeader, renderFooter }: CardProps) {
return (
<div className="card">
{renderSlot(renderHeader, ({ className: string }) =>
<h1 className={className}>Default Header</h1>), { isLoading: true }}
<p>Some content here...</p>
{renderSlot(renderFooter, ({ className: string }) =>
<small className={className}>Default Footer</small>, (part) => <footer>{part}</footer>)}
</div>
);
}
// Usage
<Card renderHeader /> // Uses default header
<Card renderHeader={<h1>Custom</h1>} /> // Custom JSX
<Card renderHeader={(H, { isLoading }) => <H />} /> // Render prop
<Card renderFooter={false} /> // No footer
<Card renderHeader renderFooter={visible} /> // Header and conditional footerMIT



