Skip to content
LogoLogo

API

Two entry points. create-slot/core is React-free — manifests, descriptors and the resolver — importable from server components, Node scripts and tests. The root entry is the React adapter, and it re-exports the core, so a client module needs one import. Hover any identifier below — the types come from the published package, not from prose.

import {
  ContributionBoundary,
  createSlot,
  definePlugin,
  defineSlot,
  entriesOf,
  resolvePlugins,
  SlotHost,
  SlotProvider,
  useContribution,
  useSlotProps,
} from "create-slot"
ExportEntryWhat it is
defineSlotcoreCreates a slot descriptor: name, contribute, override.
definePlugincoreIdentity plus an id check. Your own fields keep their types.
resolvePluginscoreThe whole registry as one pure function: plugins in, Resolution out.
entriesOfcoreThe entries of one slot, typed by its descriptor.
SlotProvideradapterPuts one Resolution in scope for every host below it.
SlotHostadapterRenders every contribution to a slot, in resolved order.
useSlotPropsadapterThe nearest host's props for a slot.
useContributionadapterWhich contribution is rendering: slot, plugin id, contribution id.
ContributionBoundaryadapterThe per-contribution isolation wrapper, for hand-rolled hosts.
createSlotfaçadeThe runtime channel, whole and alone — SPA-only, source-compatible with 2.x/3.x.

Everything that can throw is listed on one page, with its cause and its fix — see Errors.

defineSlot

const NavMenuArrow
= <{ : string }>("nav-menu")

A slot is a descriptor — pure data, safe to import from server modules. Type safety lives on this object, because a string id cannot carry Props.

The name must be non-empty: the Resolution keys contributions by name, so an empty one is two slots quietly sharing a bucket.

slot.contribute(id, spec)

// Data plus a component, under a required id. The full id "pricing/nav-item"
// is the React key, the disable/override address, and the diagnostics name.
const  = .("nav-item", {
  : 10,
  : ,
})
argumenttypenotes
idstringRequired. Local to the plugin, never contains /, unique inside it. The full id ${pluginId}/${id} is the React key and the override address.
spec.componentComponentType<Props>Receives the host's props as its own. May accept fewer; never different ones.
spec.ordernumberDefaults to 0. A priority, not an array index — see Ordering.

The return value is plain data — { slot, id, order, component } — which is what lets the resolver enumerate it before render. A malformed id is reported by the resolver as a diagnostic, not thrown at declaration.

slot.override(target, patch)

A typed patch aimed at one full contribution id. It comes from the slot rather than a string because this is where a replacement component is checked against the slot's Props.

// The application re-ranks a contribution it does not own by its full id.
// Typed patches come from the slot: `override` is where a replacement
// component would be checked against the slot's props.
export const  = .("export-csv/import", { : 5 })

definePlugin

Identity plus a non-empty-id check — and the one place where the plugin system is discoverable in a codebase. It is generic over its argument, so your own fields keep their types.

export const  = ({
  : "pricing",
  : [],
  // Any further field is yours. The library reads `id` and `contributes` only.
  : "Pricing",
})
 
// Inference keeps your own fields.
export const labelArrow
= .

The library reads id and contributes. Everything else on the object is yours; see Plugin state.

resolvePlugins

The whole registry as one pure, synchronous, deterministic function: grouping, disable, override patches, sorting (order, then plugin position, then declaration position), key minting. Inputs are never mutated. The same plugins and options produce a deep-equal Resolution every time — which is the entire SSR contract.

// One pure, synchronous, deterministic function. Problems come back on
// `diagnostics` — never thrown, never silently dropped.
const  = ([], {
  : { : [] },
  : [.("pricing/nav-item", { : 5 })],
})
 
export const  = .
optiontypenotes
disable.pluginsreadonly string[]Plugin ids to drop whole.
disable.contributionsreadonly string[]Full contribution ids to drop one by one.
overridesreadonly Override[]Typed patches from slot.override(). Later patches to one target win.

Problems come back on resolution.diagnostics — never thrown, never silently dropped. Six codes: duplicate-plugin-id, duplicate-contribution-id, invalid-contribution-id, unknown-disable-target, unknown-override-target, override-slot-mismatch. In development the provider prints them once per content change; in a test, expect(resolvePlugins(PLUGINS).diagnostics).toEqual([]) is the catalog validator.

entriesOf

// The entries of one slot, typed by its descriptor — what a hand-rolled or
// server host maps over.
export const navEntriesArrow
= (, )

Restores what contribute erased: each entry's component is typed by the slot, no cast. This is what a hand-rolled host — including a server host — maps over.

SlotProvider

function ({ ,  }:  & { : () => void }) {
  return (
    < ="button" ={}>
      Retry {}
    </>
  )
}
 
export function ({  }: { : string }) {
  return (
    <
      ={}
      ={}
      ={}
    >
      <>
        < ={} ={{ :  }}>
          <>No plugins installed</>
        </>
      </>
    </>
  )
}
proptypenotes
resolutionResolutionThe pre-resolved graph. Identity-compared: hold it at module scope or in useMemo.
onError(error: SlotError) => voidReported when a contribution throws. Inline arrows are free.
FailedComponentType<SlotError & { reset }>Rendered in place of a contribution that threw; reset retries it.
PendingComponentType<ContributionInfo>Rendered while a deferred contribution loads. Unset keeps null.

Failed and Pending are components, not render props: a component reference has stable identity and crosses an RSC boundary, where a closure cannot. The handlers live in a context of their own, read only where a contribution is isolated, so inline values never re-render a host.

SlotHost

Renders every contribution to a slot, in resolved order. Its own children are the placeholder while nothing is contributed.

proptypenotes
slotSlot<Props>Which slot to render.
propsPropsAn explicit bag, not a spread — the host's own props can never collide with a slot's, and children structurally cannot leak into a contribution. Optional when Props is empty.
childrenReactNodeThe placeholder. Renders only while nothing is contributed.
renderEntries(entries: readonly HostEntry[]) => ReactNodeFull-ownership escape hatch — see below.

Host props are value-held: the host keeps them stable while their values stay the same, and it compares each resolved entry by content, so a host that re-renders for someone else's sake does not re-render the contributions. A host outside a SlotProvider throws.

renderEntries

Called with every entry — with zero too — and owns layout, wrappers and the empty state; children is then ignored. Each HostEntry carries the identity (key, pluginId, contributionId, order) plus node, the contribution already wrapped in memo, error boundary, Suspense and identity. Key your own wrappers with entry.key, never with an array index.

// Full-ownership escape hatch: `renderEntries` owns layout, wrappers and the
// empty state. Key wrappers with `entry.key`, never with an array index.
export function ({  }: { : string }) {
  return (
    <
      ={}
      ={{ :  }}
      ={() =>
        . === 0 ? (
          <>Nothing contributed</>
        ) : (
          <>{.(() => .)}</>
        )
      }
    />
  )
}

useSlotProps

// The nearest host's props for this slot; null outside one.
export function () {
  const  = ()
 
  return  ? <>{.}</> : null
}

Returns Props | null — null outside a host. A declared contribution rarely needs it: the host's props arrive as its own. It exists for components nested deeper inside a contribution, and it is context, so it is client-only — a server host passes serializable props directly.

useContribution

export function () {
  // The identity of the contribution rendering now. Throws outside one.
  const { , ,  } = ()
 
  return (
    < ={} ={} ={} />
  )
}

Throws outside a contribution. Use it to give each plugin a store, a prefixed logger, a settings namespace, or a telemetry tag.

ContributionBoundary

The per-contribution isolation wrapper the default host places around every entry: contribution identity, an error boundary, and a Suspense boundary. Exported so hand-rolled hosts — including RSC server components mapping a Resolution — keep the same failure semantics as the default host.

// The isolation wrapper the default host places around every contribution —
// exported so a hand-rolled host keeps the same failure semantics.
export function () {
  return (, ).(() => {
    const  = .
 
    return (
      <
        ={.}
        ={.}
        ={.}
        ={.}
      >
        < ="/" />
      </>
    )
  })
}

createSlot

The runtime channel, whole and alone — SPA-only by design: the server and hydration snapshots are always empty. Each factory owns a private store, shared with nothing; the registry never merges it.

const MenuArrow
= <{ : string }>()

<Slot order?>

The component itself is the contributor. It renders nothing where it sits and takes exactly one element as its child.

// The component itself is the contributor. `children` is one element.
export function () {
  return (
    < ={10}>
      < />
    </>
  )
}
proptypenotes
childrenReactElementRequired. Exactly one element — not text, not siblings.
ordernumberDefaults to 0. Read once, on mount.

Both are also the React key's source: the key is assigned on mount and kept, so changing the child's content reconciles instead of remounting.

<Slot.Host {...props}>

// The host renders every mounted fill, or its own children while there are none.
export function ({  }: { : string }) {
  return (
    <>
      <. ={}>
        <>Placeholder</>
      </.>
    </>
  )
}

Unlike SlotHost, the façade host spreads its props — the 2.x shape — and tolerates rendering with no provider anywhere, because the façade has none.

Slot.useProps()

function () {
  // The props of the host doing the rendering. Never null: a fill's children
  // only ever render inside a host.
  const {  } = .()
 
  return <>{}</>
}

Unlike useSlotProps, this one is not nullable: a Slot's children only ever render inside one of its factory's hosts, so the façade can promise the props.

Types

All exported from the root entry; the core types also from create-slot/core.

TypeWhat it describes
Slot<Props>What defineSlot returns: name, contribute, override. Pure data.
ContributionSpec<Props>The second argument to contribute: { component, order? }.
ContributionWhat contribute returns: { slot, id, order, component }. Plain data.
OverrideWhat slot.override returns. Opaque data; resolvePlugins consumes it.
PluginDefinitionThe two fields the library reads: id, contributes?.
ResolveOptionsThe options bag: disable?, overrides?.
ResolutionThe resolved graph: slots (name → sorted entries) plus diagnostics.
ResolvedEntry<Props>One entry: key, pluginId, contributionId, slot, order, seq, component.
DiagnosticA problem the resolver found. Returned, never thrown.
ErasedComponentA contribution's component with its props erased, so one graph holds every slot's.
SlotProviderPropsresolution, onError?, Failed?, Pending?, children?.
SlotHostProps<Props>slot, props, children?, renderEntries?.
HostEntryWhat renderEntries receives: identity plus the finished node.
SlotError{ pluginId, contributionId, slot, error } — what onError and Failed receive.
ContributionInfo{ slot, pluginId, contributionId } — what useContribution returns and Pending receives.
RuntimeSlot<Props>What createSlot returns: the contributor, plus Host and useProps. Named Slot<Props> before 4.0.

PluginDefinition is the one to extend. definePlugin is generic over its argument, so an intersection type keeps every field of your own:

// `PluginDefinition` is the type to extend. `definePlugin` is generic over its
// argument, so an intersection keeps every field of your own.
type  =  & {
  : string
  ?: readonly string[]
}
 
const  = ({
  : "billing",
  : "Billing",
  : ["billing:read"],
}) satisfies 
 
export const billingPermissionsArrow
= .

Behavioural notes

  • A host with no contributions renders its own children.
  • Multiple hosts of one slot each render the same contributions; useSlotProps reflects the host doing the rendering.
  • order is a priority. Equal values are a stable tie: plugin position in the list, then declaration position. A façade fill reads its order once, on mount.
  • Entries are compared by content, because resolvePlugins mints fresh entry objects on every call and an inline call per render is legal. A host holds its props steady while their values are unchanged. See Performance.
  • Each createSlot() factory owns a private store: two factories can never exchange fills, two React roots using one factory always do, and no module-level registry exists for a duplicated package copy to split.