Plugin setup
islandsPlugin() is an SSRPlugin for @sigx/server-renderer. On the server it intercepts client:* components, captures their signal state, and injects the hydration data; on the client it schedules deferred hydration.
Registering the plugin
Install it on your app with .use():
import { createSSR } from '@sigx/server-renderer';
import { islandsPlugin } from '@sigx/ssr-islands';
import { defineApp } from 'sigx';
import { App } from './App';
// Minimal HTML shell — the rendered app replaces the outlet marker.
const template = `<!DOCTYPE html><html><body><!--ssr-outlet--></body></html>`;
const app = defineApp(<App />).use(islandsPlugin());
const html = await createSSR().renderDocument(app, { template }); // or .render(app) for a shell
That single registration wires both the server hooks (during render) and the
client hooks (during hydration). On the client you still register the island
components and call hydrateIslands() — see
Registry & code splitting.
What it does
During render, the plugin:
- maps each
client:*directive onto a boundary record viaresolveBoundary, - captures the component's signal state (so the client can restore it instead of re-fetching),
- manages async streaming for islands, and
- writes each island's strategy, props,
stateand (optionally) chunk reference into the core boundary record — serialized into the page aswindow.__SIGX_BOUNDARIES__.
During hydration, it schedules each recorded boundary according to its strategy
(load / idle / visible / media / interaction / only). When an island
hydrates it resumes from the server-captured signal state on its boundary
record's state instead of re-initialising — each signal is seeded back to its
server value. A client:only island has no captured state, so it simply mounts
fresh.
Automatic state keys
Signal state transfers by key, and the keys are derived for you — they are not
part of your component's API. The sigxIslands() Vite transform
reads the declaration identifier of each signal and uses it as the key:
export const Counter = component((ctx) => {
const count = ctx.signal(0); // transform keys this "count"
return () => <button onClick={() => count.value++}>{count.value}</button>;
}, { name: 'Counter' });
The server captures count under "count"; the client restores it from the same
key. The rule is named = transferred: a signal the transform can key travels
from server to client, and a signal it can't — a bare signal() import, or a call
that isn't a simple declaration — is just plain local state, created fresh on each
side from the same initial value.
Two properties make this safe:
- Keys are namespaced per island boundary, so two different islands can both
call their signal
state, and two instances of one island each keep their own. - Within a single island a duplicate key is first-wins — a later signal with the same name stays local, with a dev warning — so restoration can never map a value onto the wrong signal.
The upshot: any server/client asymmetry degrades to not transferred, never to
restoring a wrong value. This matches the rest of the family — useData's content
key, defineStore(name) — where state identity is explicit or there is no
transfer.
Options
islandsPlugin(options?) takes an IslandsPluginOptions:
interface IslandsPluginOptions {
/** Custom client-side hydration strategies, keyed by name. */
strategies?: Record<string, (el: Element, hydrate: () => void) => void>;
/** Island manifest mapping component names to their chunk URLs. */
manifest?: Record<string, { chunkUrl: string; exportName: string }>;
}
Custom strategies
Add your own scheduling strategy alongside the built-in six. A strategy receives
the element and a hydrate callback to invoke when it should run:
const app = defineApp(<App />).use(islandsPlugin({
strategies: {
// hydrate after a fixed delay
delayed: (el, hydrate) => setTimeout(hydrate, 2000),
},
}));
Manifest (chunk URLs)
Pass a manifest so islands carry their chunk URL in the hydration data — the
client can then load each chunk on demand without an eager
registerComponent() call. The manifest is generated by
sigxIslands() from @sigx/vite/islands
during the build, at .vite/sigx-islands-manifest.json:
import manifest from './dist/client/.vite/sigx-islands-manifest.json';
const app = defineApp(<App />).use(islandsPlugin({ manifest }));
With a manifest, each island's chunkUrl and exportName are written into its
boundary record, and loadIslandComponent() resolves the component straight from
its chunk.
Signal-state transfer (low level)
The plugin handles state capture for you. The underlying helpers are exported for
custom server pipelines (from @sigx/ssr-islands or @sigx/ssr-islands/server):
createTrackingSignal(signalMap)— asignal()replacement that records signal keys and values during setup.serializeSignalState(signalMap)— serialize the captured map onto the boundary record'sstate, for the client to restore.
Keys are the automatic keys the sigxIslands() transform
assigns, so server capture and client restore line up. For keyed
useData/useStream data the key is the call's own key, serialized into the page
automatically — see
Rendering & streaming.
