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:
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:
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:
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:
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).
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.beforeHydratereturningfalseskips the default DOM walk — the basis for resumable SSR.client.hydrateComponentreturning aNodeclaims that component — the hook islands use to interceptclient:*props and schedule deferred hydration.server.handleAsyncSetupchoosesblock,stream, orskipper async component;getInjectedHTMLandgetStreamingChunkslet 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.
