Server/Packages/Server Renderer/Hydration & head
@sigx/server-renderer · Stable

Hydration & head#

After the server sends HTML, the client re-attaches reactivity to that existing DOM. This guide covers the hydration entry point, head management, and the plugin SPI that advanced strategies build on.

The client hydration entry#

The high-level entry is the ssrClientPlugin. It adds hydrate() to your App:

TSX
import { defineApp } from 'sigx';
import { ssrClientPlugin } from '@sigx/server-renderer/client';
import { App } from './App';

defineApp(<App />)
    .use(ssrClientPlugin)
    .hydrate('#root');

hydrate() accepts a CSS selector string or an Element. It finds the root component plus AppContext, then hydrates the existing SSR DOM. If the container has no SSR content, it falls back to a fresh client render — so the same entry works for both pre-rendered and client-only pages.

Low-level hydration#

For custom setups you can call the core hydrate function directly. It normalizes the element to a VNode, sets the current AppContext for DI, runs client plugin beforeHydrate hooks, walks the DOM via hydrateNode, then runs afterHydrate:

TSX
import { hydrate } from '@sigx/server-renderer/client';
import { App } from './App';

const container = document.getElementById('root')!;
hydrate(<App />, container);

hydrateNode does the per-VNode work: in the happy path it creates no DOM, just attaches events/props/directives/refs, skips SSR comment markers (<!--t-->, <!--$c:N-->), recovers from minor SSR drift by scanning forward (with a dev warning), and returns the next sibling.

Head management#

Call useHead() inside any component to manage <head> elements. During SSR the configs are collected into the active context; on the client useHead() mutates the DOM directly and registers cleanup on unmount:

TSX
import { component } from 'sigx';
import { useHead } from '@sigx/server-renderer';

export const ArticlePage = component((props) => {
    useHead({
        title: props.title,
        titleTemplate: '%s — My Site',
        meta: [
            { name: 'description', content: props.summary },
            { property: 'og:title', content: props.title },
        ],
        link: [{ rel: 'canonical', href: props.url }],
        htmlAttrs: { lang: 'en' },
    });

    return () => <article>{props.body}</article>;
});

The render APIs auto-collect head configs during render and append the head HTML to the context (read it via ctx.getHead()). titleTemplate uses %s as the title placeholder, and renderHeadToString() dedupes meta by name/property/http-equiv/charset.

Manual head rendering#

If you manage the document yourself, the lifecycle is explicit:

TSX
import {
    enableSSRHead,
    collectSSRHead,
    renderHeadToString,
} from '@sigx/server-renderer';
import { renderToString } from '@sigx/server-renderer/server';
import { App } from './App';

enableSSRHead();                       // switch useHead() into collection mode
const body = await renderToString(<App />);
const configs = collectSSRHead();      // disable + return collected configs
const headHtml = renderHeadToString(configs);

const doc = `<!DOCTYPE html><html><head>${headHtml}</head><body><div id="root">${body}</div></body></html>`;

The render APIs already enable head collection for you; reach for these functions only when driving the lifecycle by hand.

The plugin SPI#

The core renderer and hydrator are strategy-agnostic. Every advanced hydration strategy — selective, islands, resumable, Suspense — is an SSRPlugin with optional server and client hook sets.

  • Register server hooks with createSSR().use(plugin).
  • Register client hooks with registerClientPlugin(plugin).
TSX
import { createSSR, type SSRPlugin } from '@sigx/server-renderer';

const myPlugin: SSRPlugin = {
    name: 'my-strategy',
    server: {
        // choose block | stream | skip for async components, inject HTML, etc.
        handleAsyncSetup(id, ssrLoads, renderFn, ctx) {
            return { mode: 'stream' };
        },
    },
    client: {
        // return false to skip the default DOM walk (resumable SSR)
        beforeHydrate(container) {
            return; // run normal hydration
        },
        // return a Node to "claim" a component during the hydration walk
        hydrateComponent(vnode, dom, parent) {
            return undefined; // let core hydrate it
        },
    },
};

const ssr = createSSR().use(myPlugin);
const html = await ssr.render(<App />);

Key hook semantics:

  • client.beforeHydrate returning false skips the default DOM walk — the basis for resumable SSR.
  • client.hydrateComponent returning a Node claims that component — the hook islands use to intercept client:* props and schedule deferred hydration.
  • server.handleAsyncSetup chooses block, stream, or skip per async component; getInjectedHTML and getStreamingChunks let a plugin emit extra markup or streamed chunks.

You usually don't write this by hand. The islands strategy — client:* directives, deferred hydration, signal-state transfer and per-island code splitting — is already implemented as a plugin in @sigx/ssr-islands. Reach for the raw SPI only when building a new strategy.

Next steps#