Slots, hosts & fills
Four words, used consistently everywhere in this library:
- Slot — a named extension point. A name plus a props type, never a
component — and since 4.0, literally:
defineSlotreturns a plain descriptor. - Host — the component that renders every contribution to one slot
(
SlotHost). Its own children are the placeholder. A slot can have many hosts mounted at once. - Contribution — one piece of UI addressed to a slot, declared with a required id.
- Fill — the runtime channel's contributor: the element a
createSlot()slot registers while mounted. A façade-only concept.
Defining a slot
import { } from "create-slot/core"
export const = <{ : string; : string }>(
"deal-actions",
)The name is the Resolution's key, so it must be non-empty and unique across
your application. Keep slot definitions in their own module: it is the only
thing both the layout and the features import, which is what keeps them from
importing each other. The descriptor is pure data from the React-free
create-slot/core, so server modules can import it too.
The type parameter is the host's props — whatever the place that renders
contributions knows and a contribution might want. children is not part of
it; children belong to the host.
Mounting a host
import { } from "create-slot"
export function ({
,
,
}: {
: string
: string
}) {
return (
< ="toolbar">
< ={} ={{ , }}>
{/* The placeholder. It renders only while nothing is contributed. */}
<>No actions available</>
</>
</>
)
}props is an explicit bag, not a spread — so the host's own props (slot,
children, renderEntries) can never collide with a slot's, and children
structurally cannot leak into a contribution.
The host's children are the placeholder, and there is no separate API for one.
They render while nothing is contributed — which is not the same as nothing
producing output. A slot whose contributions all returned null has no
children rendered and no DOM either, so the container really is empty; see
the empty-state recipe.
A host without a SlotProvider above it throws — a missing provider is a
mistake worth reporting, not an empty slot.
Contributing
A declared contribution is an ordinary component. It receives the host's props
as its own, and it decides its own visibility with a plain if — there is no
when predicate.
export function ({
,
,
}: {
: string
: string
}) {
// Visibility is a plain `if`. There is no `when` predicate.
if ( !== "closed") {
return null
}
return < ="button">Archive {}</>
}The runtime channel
The runtime channel lives inside the createSlot() façade:
the factory's slot component contributes its child element from wherever it is
mounted, for as long as it is mounted. The element is written next to the
feature but rendered where the host is, so it reads the host's props through
useProps rather than receiving them.
// The runtime channel lives in the createSlot() façade: the factory's slot
// component registers its child while mounted, and the child reads the host's
// props through `useProps` — the element is written here but rendered where
// the host is.
import { } from "create-slot"
const = <{ : string }>()
export function () {
return (
< ={20}>
< />
</>
)
}
function () {
const { } = .()
return < ="button">Call about {}</>
}Two things worth knowing:
- A façade's
usePropsis never null — aSlot's children only ever render inside one of its factory's hosts. The adapter'suseSlotProps(slot)is the nullable one, because nothing guarantees a host above its caller. orderand the element's React key are read once, on mount. Changingorderlater leaves the fill in place; changing its content reconciles instead of remounting. See Ordering.
Many hosts, one contribution
A slot can have any number of hosts mounted at once. Each renders the same contributions, and each hands them its own props — which is what makes one contribution adapt per row.
export function ({
,
}: {
: { : string; : string }[]
}) {
return (
<>
<>
{.(() => (
< ={.}>
<>{.}</>
<>
{/* One slot, one host per row: the same contribution renders in
every mounted host, each time with that host's own props. */}
<
={}
={{ : ., : . }}
/>
</>
</>
))}
</>
</>
)
}Typing a slot
The type parameter is the host's props, and the props bag is checked against it at the call site rather than at render:
// No type parameter: a host with no props bag to pass.
const = < ={}>Ready</>
// A typed slot makes the bag required…
const = < ={} />
// …and rejects a prop the slot does not declare.
const = < ={} ={{ : "/", : "x" }} />A contribution's own parameter type has to accept what the host provides. It may accept less — that is ordinary assignability — but never something different:
// Fewer props than the host provides: fine.
function ({ }: { : string }) {
return < ="button">Archive {}</>
}
const = .("archive", { : })
// A prop the host does not have: rejected.
function ({ }: { : string }) {
return < ={}>Open</>
}
const = .("bad", { : })Hover contribute to see what it produces — plain data under a required id,
which is what makes the declarative channel server-renderable and addressable:
// `contribute` produces plain data under a required id — which is what a
// server render enumerates, and what `disable` and `override` address.
export const = .("archive", {
: ,
})Next
- Two channels — which channel a given contribution belongs on
- Plugin registry — the declarative channel end to end
- Ordering —
orderas a priority, ties, and the read-once rule - Recipes — per-row hosts, empty states, exclusive claims
- Errors — every message the library throws, and its fix