Shell & mounting
The shell side is @preact-mvc/atoll-island (the package root): mount an island as an element, pass props and callbacks, receive events. Three mounting surfaces cover the common cases.
islandComponent(contract) — the facade
The recommended mount. The contract module names the app, declares the wire schemas, and carries the worker factory — so the call site passes no worker plumbing, and props/events typecheck against the contract:
import { islandComponent } from '@preact-mvc/atoll-island';
import { tickerContract } from './ticker.contract';
const Ticker = islandComponent(tickerContract);
// onEvent narrows to the contract's declared events; island props are
// the contract's props schema. No worker prop — the contract carries it.
<Ticker
source="orders"
onEvent={(name, payload) => {
if (name === 'worker-ping') notify(payload);
}}
/>The string-keyed form still exists —
islandComponent('ticker')— for registries whose workers the shell already wires up.
<Island> — the primitive
import { Island } from '@preact-mvc/atoll-island';
import type { WorkerLike } from '@preact-mvc/atoll-island';
const tickerWorker = (): WorkerLike =>
new Worker(new URL('./ticker.worker.tsx', import.meta.url), {
type: 'module',
}) as WorkerLike;
<Island
app="ticker" // registry key, stamped ref, or contract
worker={tickerWorker} // omit when app is a contract with worker
props={{ source: 'orders' }} // structured-cloneable
onEvent={(name, payload) => log(name, payload)}
mode="poll" // 'push' needs COOP/COEP isolation headers
onReady={(handle) => handle.updateProps({ source: 'live' })}
onError={(err) => show(err)}
className="metric-card"
/>worker accepts a WorkerLike directly or a factory — use the factory form in real code so Vite detects the entry. When app is a contract that carries a worker, the contract module is the whole connection. Unmounting the element terminates an owned worker.
Shared workers: one client, many islands
A poly worker can host several islands at once — connect once, hand the client to each mount:
import { connectIslandWorker, Island } from '@preact-mvc/atoll-island';
const client = connectIslandWorker({
worker: () =>
new Worker(new URL('./dash.worker.tsx', import.meta.url), {
type: 'module',
}) as WorkerLike,
});
// Each mount is an isolated instance inside the shared worker:
<Island client={client} app="ticker" props={{ source: 'orders' }} />
<Island client={client} app="ticker" props={{ source: 'payments' }} />
<Island client={client} app="metrics" />Instances are keyed app@N in the worker — separate view trees, separate state, separate props, one worker process.
lazyIsland — code-split facades
Lazy-loads a module that exports a contract (or a stamped app) — ideal for keeping a contract's worker factory and schema out of the initial bundle's critical path:
const HeavyChart = lazyIsland(() => import('./heavy-chart.contract'));
// The contract module loads on first mount; fallback renders meanwhile.
<HeavyChart dataset="q4" fallback={<Skeleton />} />If you reference a stamped app instead of a contract, import it with
import typein shell code — a worker definition must never leak into the main-thread bundle (defining one installs instance globals).
Imperative: mountIsland
For non-JSX integration (a portal, a vanilla container, tests), mountIsland attaches to any element and returns a handle:
const handle = await mountIsland({
el,
worker: tickerWorker,
app: 'ticker',
props: { source: 'orders' },
onEvent: (name, p) => console.log(name, p),
});
handle.updateProps({ source: 'payments' }); // re-render with new props
handle.destroy(); // teardown + worker cleanupEvents back to the shell
Everything the worker emit()s arrives at onEvent(name, payload); shell functions cross in the other direction via callbackProp — details in Island apps. Instance scoping and the op wire are in How it works.