Server rendering
Render a request-scoped router, serialize its snapshot, then hydrate with the browser route table.
Compatibility
Interactive shells with deferred data
Both renderApplication entries accept earlyHydration: true. React DOM 19+ uses native Suspense and use(promise); Preact 11 uses Await with an explicit fallback or inside Preact Suspense. Shell controls hydrate while nested promises remain pending, without repeating the initial loader. The default hydrates after document completion. Vite development transformation buffers HTML, so test interactivity with a production server.
return renderApplication(request, routes, root, entry, {
earlyHydration: true,
nonce,
});Deferred timeout compatibility in 0.4.5
In 0.4.5, the default initial deferred-data budget changes from 30 seconds to 8 seconds for both React and Preact. It starts when the hydration payload is revived; individual settlements do not restart it. For intentionally slower initial data, set deferredTimeout explicitly. This bounds silent document interruptions, not immediate disconnect detection, and does not retry loaders or mutations.
// Use the matching react/client or preact/client entry.
await mountApplication(routes, root, {
deferredTimeout: 30_000,
});Integrated React documents
Use matching react/server, react/client and react/document imports with React DOM 19+. Preact uses the corresponding preact paths. Routing hooks and manual loadRouter SSR continue to support React 18. Application middleware, scoped data endpoints and lazy file discovery work with either renderer.
// Server
import { renderApplication } from "@kasperrt/smolter/react/server";
// Browser
import { mountApplication } from "@kasperrt/smolter/react/client";
// Root Layout
import { Document, Meta, Links, Scripts } from "@kasperrt/smolter/react/document";Load on the server
Create a router per request. Passing a Request forwards headers and cancellation. Avoid sharing router state or user data between requests.
import { loadRouter, serialize } from "@kasperrt/smolter/react/server";
const router = await loadRouter(serverRoutes, request);
const html = renderToString(<RouterProvider router={router}><Routes /></RouterProvider>);
const hydration = serialize(router.getSnapshot());
// Embed hydration as script text in the HTML response.Hydrate
deserialize restores Error objects. A successful hydration snapshot avoids a duplicate initial loader request. Client-only routes render their fallback on the server and load in the browser.
import { deserialize } from "@kasperrt/smolter/react/client";
const hydrationData = deserialize(
document.querySelector("#smolter-data")!.textContent!,
);
const router = createDataRouter(browserRoutes, { browser: true, hydrationData });
hydrateRoot(document.querySelector("#app")!,
<RouterProvider router={router}><Routes /></RouterProvider>,
);Production transport
The browser proxy appends .__smolter to the route pathname and preserves its query. Vite and createRequestHandler normalize it before dispatch. Application prefixes remain in the path. Explicit dataEndpoint retains legacy query transport when configured consistently.
Hosting
GitHub Pages serves the browser examples and documentation. It cannot run server loaders. Run apps/react-ssr or apps/preact-ssr on a Node server to try request-time rendering and server mutations.
Deferred Preact documents
renderApplication starts root and route loaders concurrently. Top-level loader promises determine the initial data and HTTP status; nested promises can remain pending. For early hydration, Await with an explicit fallback renders ordinary fallback HTML and streams results as data. Await inside Preact Suspense is also supported: application outlets use inert template anchors to track the original route’s stream markers. Hydration renames the outlet’s stream targets while retaining the boundary comment structure. Component changes remove abandoned markers, including later nested markers; shared components and root boundaries retain ownership. Without early hydration, resolved HTML still streams. With earlyHydration enabled, pending references arrive with the shell and deferred successes or failures settle in the browser independently. An explicit stream end rejects unfinished values. An abruptly interrupted document may emit no browser completion event; the initial deferred-data deadline then rejects remaining values after 8 seconds by default. Set mountApplication options.deferredTimeout for a longer initial-data budget. No requests or mutations are replayed. Root-loader failures retain root-boundary ownership during hydration. Errors after headers are sent cannot change the HTTP status.
Root stylesheets and route CSS
Route asset manifests collect static CSS imports from matched route chunks. A root import such as index.css?url provides an asset URL, not a route CSS dependency; an empty styles array is expected when all CSS is loaded this way. Keep rendering the root stylesheet link. Smolter does not infer stylesheet use from arbitrary URLs. If measurements show late discovery, the host can send an explicit preload or Early Hint for the same resolved URL and matching CORS mode. A preload fetches CSS but does not apply it. Do not restructure CSS merely to populate the manifest.
import stylesUrl from "./index.css?url";
// Keep this in the root document head.
<link rel="stylesheet" href={stylesUrl} />Externally injected head elements
Whole-document hydration requires matching server/client layouts. Mark externally owned direct children of head with data-smolter-preserve; do not also render them in the application. Preact temporarily excludes these nodes during hydration. React 19 uses its native head reconciliation. Tests retain marked scripts/styles through hydration, revalidation and navigation. This is not permission for arbitrary body injection or changes to the hydration payload.
<script data-smolter-preserve src="/infrastructure-instrumentation.js"></script>
<meta data-smolter-preserve name="infrastructure-tag" content="example">Client promises versus server transport
Deferred clientLoader data in 0.4.6 stays in browser memory and needs neither earlyHydration nor the SSR promise protocol. Server loader HTTP responses still resolve nested values before JSON serialization. A clientLoader can await serverLoader() for required data and return separate optional promises. SSR behavior and the 8-second initial hydration budget are unchanged.