SSR and hydration
Server render contract, Declarative Shadow DOM, hydration state, and React/Vue helpers.
See versions, changes and requirements in Framework 4.0 and CLI 1.2.
What the server produces
wu-framework/server transforms a renderer’s result into WU container HTML. By default, it encapsulates content and styles in Declarative Shadow DOM. The browser can materialize that tree before the app’s JavaScript loads.
The server receives a module with render(props), not the client’s mounting lifecycle. The result can be an HTML string or { html, styles?, state? }. Whoever generates the HTML must escape untrusted data before including it.
import { renderApp, escapeHtml, renderStateScript } from 'wu-framework/server';
const result = await renderApp('greeting', {
props: { name: 'Ada' },
module: {
render(props) {
return {
html: `<p>Hola, ${escapeHtml(props.name)}</p>`,
styles: 'p { color: #f97316; }',
state: props,
};
},
},
});
const html = result.html + renderStateScript({ greeting: result.state });
renderApp also accepts an already imported module, a promise, a default module, or a function that loads it. The host decides which modules may run on the server; do not turn user input into an arbitrary import.
Result and options
| Element | Contract |
|---|---|
result.html |
Complete container |
result.inner |
Declarative or flat interior |
result.containerAttrs |
ID, data-wu-app, data-wu-ssr, optional class |
result.state |
Optional state returned by the renderer |
containerId |
Defaults to wu-app-<name> |
shadowMode |
Defaults to open; closed prevents normal hydration |
declarative |
Defaults to true |
styles |
Extra host CSS for that app |
nonce |
CSP nonce for that response’s styles |
renderApps processes multiple apps in parallel. By default, it records individual failures in errors and preserves the others; failFast: true allows aborting. A renderer failing in renderApp produces an error with code WU_SSR_RENDER_FAILED.
Hydrate without erasing
On the client, the shell registers the app normally and mounts it in its SSR container. The sandbox detects data-wu-ssr and the data-wu-root root. If lifecycle.hydrate exists, the core invokes it with server props and the scoped facade. Without that slot, it calls mount, and the existing HTML serves as initial content until the renderer replaces it.
wu.define('greeting', {
mount(container) {
container.textContent = 'Montaje cliente';
},
hydrate(container, { props }) {
// Adoptar lo que ya existe. No limpiar container primero.
const paragraph = container.querySelector('p');
paragraph?.setAttribute('aria-label', `Saludo para ${props.name}`);
},
unmount(container) {
container.replaceChildren();
},
});
For React, hydrateRoot must receive the same tree/props the server produced. For Vue, the adapter uses createSSRApp and .mount to adopt the existing DOM. Published state must preserve that correspondence; changing data between rendering and hydration causes mismatches.
Renderer helpers
The server subpath exports renderReact, renderPreact, renderVue, renderSvelte, renderSolid, and renderHtml. Helpers return a module conforming to the render contract.
import * as React from 'react';
import { renderToString } from 'react-dom/server';
import { renderReact } from 'wu-framework/server';
import App from './App.jsx';
export default renderReact(App, {
react: React,
renderToString,
styles: '.greeting { font-weight: 600; }',
});
Injecting the renderer and runtime avoids resolving another React copy outside the server bundle. Vue supports injection of createSSRApp and renderToString, plus setup. Server option names are not identical to the client adapter’s runtime: { React, ReactDOM }.
Astro
---
import WuAppSSR from 'wu-framework/integrations/astro/WuAppSSR.astro';
import greetingServer from '../apps/greeting/server.js';
---
<WuAppSSR
name="greeting"
module={greetingServer}
props={{ name: 'Ada' }}
fallbackToClient={false}
/>
The shell must still initialize the corresponding client registration. The component publishes state as JSON for hydration; hydrate defaults to true. fallbackToClient also defaults to true: an SSR failure leaves the slot for client mounting unless you disable it as in the example.
In a static Astro build, the renderer runs at build time; it does not turn that page into per-request personalized content. User-dependent data requires an appropriate server or client-loading strategy.
State and headers
serializeState and renderStateScript handle serialization for the HTML/script context. escapeHtml escapes text; it does not validate an arbitrary HTML application. Do not inject secrets into hydration state: everything published on that page is visible to its reader.
createNonce() produces a nonce for one response. securityHeaders, applySecurityHeaders, wuSecurityMiddleware, and withSecurityHeaders help install CSP and other headers. Use the same response nonce in styles/scripts that must run and a new nonce for the next response; the server controls that coordination.
Implementation references
Exports: src/server/index.js; rendering: src/server/wu-server-render.js; multiple renders: src/server/wu-server-render.js; types: src/server/index.d.ts; DOM adoption: src/core/wu-sandbox.js; hydrate slot: src/core/wu-core.js; Astro: integrations/astro/WuAppSSR.astro. Tests: tests/unit/server-render.test.js, tests/unit/server-security-fuzz.test.js, tests/unit/security-headers.test.js.
To isolate each request and bound work, see createServerRequest, private loaders, cancellation and concurrency in the SSR reference. The 4.0 header wrappers reject a fixed nonce: use a per-request function or the default generation.