Skip to content
LogoLogo

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: defineSlot returns 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 useProps is never null — a Slot's children only ever render inside one of its factory's hosts. The adapter's useSlotProps(slot) is the nullable one, because nothing guarantees a host above its caller.
  • order and the element's React key are read once, on mount. Changing order later 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  = < ={} />
Property 'props' is missing in type '{ slot: Slot<{ current: string; }>; }' but required in type '{ props: NoInfer<{ current: string; }>; }'.
// …and rejects a prop the slot does not declare. const = < ={} ={{ : "/", : "x" }} />
Object literal may only specify known properties, and 'href' does not exist in type '{ current: string; }'.

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", { :  })
Type '({ href }: { href: string; }) => Element' is not assignable to type 'ComponentType<{ dealId: string; stage: string; }>'. Type '({ href }: { href: string; }) => Element' is not assignable to type 'FunctionComponent<{ dealId: string; stage: string; }>'. Types of parameters '__0' and 'props' are incompatible. Property 'href' is missing in type '{ dealId: string; stage: string; }' but required in type '{ href: string; }'.

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 — order as 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