Server/Packages/Islands/Plugin setup
@sigx/ssr-islands · Stable

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():

TSX
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 via resolveBoundary,
  • 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, state and (optionally) chunk reference into the core boundary record — serialized into the page as window.__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:

TSX
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:

TypeScript
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:

TSX
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:

TSX
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) — a signal() replacement that records signal keys and values during setup.
  • serializeSignalState(signalMap) — serialize the captured map onto the boundary record's state, 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.

Next steps#