Skip to content
LogoLogo

Migrating to 4.0

4.0 rebuilt the registry around one pure function. In 3.x the provider built an index from the plugin array during render; in 4.0 the application calls resolvePlugins itself and hands the Resolution to the provider. Hosts became one generic component, contributions gained a required id, and the runtime channel moved entirely into the createSlot() façade — the registry never merges fills.

The map

3.x4.0
<PluginProvider plugins={plugins}>resolvePlugins(plugins) + <SlotProvider resolution={resolution}>
slot.contribute({ order, component })slot.contribute("id", { order, component }) — the id is required
<Nav.Host current={route}><SlotHost slot={Nav} props={{ current: route }}> — an explicit bag, not a spread
<Nav.Fill order={20}>the createSlot() façade — a descriptor has no Fill
slot.useProps()useSlotProps(slot)
usePluginId()useContribution().pluginId
renderFailed={({ reset }) => …}Failed={FailedComponent} — a component, receiving { pluginId, contributionId, slot, error, reset }
onErrorunchanged, now also reports contributionId
—Pending={PendingComponent} — the fallback while a deferred contribution loads
type SlotDefinition<Props>type Slot<Props> — the descriptor: { name, contribute, override }
type Slot<Props> (the façade's)type RuntimeSlot<Props>
type PluginErrortype SlotError
positional React keys (pluginId#index)stable full ids (pluginId/contributionId)
dev-only duplicate-id warningsix diagnostics on resolution.diagnostics
—entriesOf, override, disable, ContributionBoundary, create-slot/core

The façade is unchanged

createSlot, Slot, Slot.Host and Slot.useProps are the names 2.x published, and they still mean what they meant — no codemod, no deprecation. One type moved: Slot<Props> now names the descriptor defineSlot returns, and the façade's return type is RuntimeSlot<Props>.

// Nothing to do at runtime. `createSlot`, `Slot`, `Slot.Host` and
// `Slot.useProps` are the names 2.x published, and they still mean what they
// meant.
const  = <{ : string }>()
 
export function () {
  return (
    < ={10}>
      <>Pricing</>
    </>
  )
}
 
// One rename: the type 2.x called `Slot<Props>` is now `RuntimeSlot<Props>`.
// `Slot<Props>` names the descriptor `defineSlot` returns instead.
export type  = <{ : string }>

Provider and host

Before, the provider built the index during render:

// 3.x
<PluginProvider plugins={plugins} onError={report}>
  <NavMenu.Host current={route} />
</PluginProvider>

Now the application resolves, and the provider distributes:

// The 4.0 shape of a v3 registry: the contribution gains a required id, the
// provider takes a Resolution, and the host is one generic component with an
// explicit props bag.
const  = <{ : string }>("nav-menu")
 
const  = ({
  : "pricing",
  : [
    .("nav-item", { : 10, :  }),
  ],
})
 
function ({  }: { : string }) {
  return <>{}</>
}
 
const  = ([])
 
export function ({  }: { : string }) {
  return (
    < ={}>
      <>
        < ={} ={{ :  }} />
      </>
    </>
  )
}

Two things this buys:

  • The Resolution is inspectable data — slot name → sorted entries plus diagnostics — so a validator is expect(resolvePlugins(PLUGINS).diagnostics).toEqual([]) and an inventory is a .map.
  • It is React-free. resolvePlugins comes from create-slot/core, so a server component can resolve and send the whole graph across the RSC boundary.

Contribution ids are required

// 3.x — keyed by position
NavMenu.contribute({ order: 10, component: PricingItem })
 
// 4.0 — keyed by id
NavMenu.contribute("nav-item", { order: 10, component: PricingItem })

The full id pluginId/contributionId is the React key, the disable and override address, and the name diagnostics use. Because the key is the id, inserting or removing a neighbouring contribution never remounts the others — the 3.x rule about fixed-shape contributes arrays is gone, along with the advice to hide conditional UI inside a component for the key's sake.

renderFailed becomes Failed

A component, not a render prop: its identity is stable, and a component reference crosses an RSC boundary where a closure cannot.

// `renderFailed` becomes `Failed` — a component, so its identity is stable
// and it crosses an RSC boundary. It also learns which contribution failed.
function ({ ,  }:  & { : () => void }) {
  return (
    < ="button" ={}>
      Retry {}
    </>
  )
}
 
export function () {
  return (
    < ={} ={}>
      < ={} ={{ : "/" }} />
    </>
  )
}

There is still no automatic reset — recovery is the reset your component is given. Pending, new in 4.0, fills the same Suspense boundary while a deferred contribution loads.

usePluginId becomes useContribution

// `usePluginId()` becomes `useContribution().pluginId`, and the hook now also
// names the contribution and the slot.
export function () {
  const { , ,  } = ()
 
  return (
    < ={} ={} ={} />
  )
}

Fills on a defined slot

A 3.x defineSlot host merged declared contributions with runtime fills. That host no longer exists: a SlotHost renders declared contributions only, so a late client fill can never displace markup the server already shipped.

A fill that was genuinely runtime — a status entry that exists only while a dialog is open — moves to a createSlot() façade slot with its own host. A fill that existed to inject known-up-front content becomes a declared contribution. See Two channels for the decision written out.

Migrating from 2.x

The façade path is unchanged since 3.0: order is a priority, not an array index — two fills that share one both render, in mount order — so order bands, strides, duplicate-order detectors and resetKey bookkeeping all stay deletable. See Ordering for the read-once rule.