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.x | 4.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 } |
onError | unchanged, 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 PluginError | type SlotError |
positional React keys (pluginId#index) | stable full ids (pluginId/contributionId) |
| dev-only duplicate-id warning | six 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.
resolvePluginscomes fromcreate-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.