npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

render-slot

v1.0.0

Published

A type-safe React render-slot pattern for composable defaults, overrides, wrappers, gateways, and SSR.

Readme

Render Slot Image

🧠 Render Slot

npm version bundle size license Types GitHub stars

Flexible Slot Rendering Utility for React

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.


📦 Installation

npm install render-slot

(or copy the renderSlot.ts, Renderable.ts and useGateway.ts files into your utils folder)


✨ API Overview

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,
    // Also merge context into the wrapped default's props
    passContextToDefault?: boolean
  }
}): ReactNode

Overloaded 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?)

The positional forms are intended for concise everyday use. In the three-argument form, a plain object shaped like { context?, wrapper?, options? } is interpreted as configuration. If context data itself uses one of those reserved keys with a configuration-compatible value, use the explicit object form:

renderSlot({
  bespoke: renderText,
  default: DefaultText,
  context: { wrapper: formatValue },
})

This keeps common calls short while providing an unambiguous form for the rare key collision.


🔑 Renderable Type

The bespoke slot property (what is passed to component that uses renderSlot) can be many different things:

export type Renderable<
  P extends object = Record<string, unknown>,
  C extends object = Record<string, unknown>,
> =
  | 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 alternatives sequentially e.g. [{ propA: 10, propB: 'string' }, <div>Text</div>]

🧩 Common Use Cases

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>);
}

1. Boolean flags

<Component renderText />  
// ✅ Renders default part

<Component renderText={false} />  
// ✅ Renders nothing

2. Nil values

<Component renderText={null} />  
// ✅ Renders nothing

<Component renderText={undefined} />  
// ✅ Renders nothing

<Component renderText={[]} />  
// ✅ Renders nothing

3. Default part fallback

<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.

4. Custom React Nodes

<Component renderText={<strong>Custom text</strong>} />
// → <strong>Custom text</strong>

5. Render prop

<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.


6. Wrapper

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.


7. Primitives with auto-wrap

renderSlot("Hello World", (props) => <span {...props} />, undefined, { wrapNonElementWithDefault: true })

// → <span>Hello World</span>

renderSlot(
  "Hello World",
  (props) => <span {...props} />,
  { className: "highlight" },
  { wrapNonElementWithDefault: true, passContextToDefault: true }
)

// → <span className="highlight">Hello World</span>

8. Rendering default implementation outside of the component

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>

For SSR, provide an initial default so <Text /> can render before the source component is reached during the server render:

const [Text, renderText] = useGateway<typeof Component, 'renderText'>({
	initialDefault: ({ propA }) => <div>Example:{propA}</div>,
})

After hydration, the gateway synchronizes with the source component's current default. Keep initialDefault visually equivalent to that source default; otherwise the gateway may visibly change when hydration completes.

Gateway lifecycle

  • When a source component unmounts, mounted <Text /> targets retain the most recently captured default.
  • A newly mounted source using the same gateway replaces that retained default.
  • If multiple sources share one gateway, the most recently committed source wins. Prefer one source per gateway when deterministic ownership matters.
  • Unmounting the component that owns useGateway releases the gateway and all of its state.

9. Array of various options

renderSlot(["Hello World", { propA: 2 }, <span>CustomText</span>])

// → Hello World<div>Example:2</div><span>CustomText</span>

🖼️ Visual Examples

Default vs Custom Rendering

| Code | Output | |-------------------------------------------------------|--------| | <MyComp renderText /> | default | | <MyComp renderText={<strong>Bold!</strong>} /> | custom | | <MyComp renderText={(Default) => <em><Default /></em>} /> | wrapped |


🚦 Decision Flow

The logic for renderSlot works like this:

  1. false | null | undefinedrenders nothing
  2. truerenders default
  3. functionrenders function(default, context)
  4. props objectrender default(props)
  5. primitive (string/number) with wrapNonElementWithDefaultrender default({children})
  6. otherwise → render as-is
  7. In case array is provided start from point 1 for every element of the array

⚡ Best Practices

  • Use renderSlot in component APIs to make them flexible without dozens of props.
  • Provide sensible defaults (don’t force users to always override).
  • Combine with wrapper for lists, layouts, and consistent styles.
  • Use wrapNonElementWithDefault to 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.

📚 Example Component

type CardProps = {
  renderHeader?: Renderable<{ className: string }, { isLoading: boolean }>;
  renderFooter?: Renderable<{ className: string }>;
};

function Card({ renderHeader, renderFooter }: CardProps) {
  return (
    <div className="card">
      {renderSlot(renderHeader, ({ className }: { className: string }) =>
      	<h1 className={className}>Default Header</h1>, { isLoading: true })}
      <p>Some content here...</p>
      {renderSlot(renderFooter, ({ className }: { className: string }) =>
      	<small className={className}>Default Footer</small>, {
          wrapper: (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 footer

📄 License

MIT