Server-Side Rendering
Weft renders on the server and hydrates on the client: the server produces HTML (plus inline data), and the browser adopts that existing DOM in place rather than re-creating it. Boundary.rpc extends this to rpc-backed server data — resolve an rpc on the server, serialize its result into the HTML, replay it on the client without a second request, and then keep the region live for refetch.
The two halves
- Server —
@weftui/dom/serverrenders an app node to an HTML string (or stream). The hydratable variants additionally emit the inline data each reactive region andBoundary.rpcneeds to resume on the client. - Client —
@weftui/dom/client'shydratewalks the server DOM, adopts it, wires up reactivity and event handlers, and resumes from the inline data. It does not re-render from scratch.
// server entry
import { renderToStringHydratable } from "@weftui/dom/server";
import { Effect } from "effect";
import { App } from "./app";
export const render = (): Promise<string> => Effect.runPromise(renderToStringHydratable(App()));// client entry
import { hydrate } from "@weftui/dom/client";
import { Effect } from "effect";
import { App } from "./app";
const root = document.getElementById("root")!;
void Effect.runPromise(hydrate(App(), root));The same side-effect-free App is imported by both entries — splice the server HTML into your template's outlet, ship it, and let the client entry hydrate it.
@weftui/dom/server exports four renderers:
| String | Stream | |
|---|---|---|
| Plain (no JS / no hydration) | renderToString |
renderToStream |
| Hydratable (emits inline payloads) | renderToStringHydratable |
renderToStreamHydratable |
Use a hydratable renderer whenever the client will call hydrate. The plain renderers produce complete, JS-free HTML with no payload scripts.
Loading server data with Boundary.rpc
SSR's natural companion is Boundary.rpc: it resolves an rpc on the server, serializes the result into the same HTML this page produces, and replays it on the client during hydrate — no second request, no fallback flash — then keeps the region live for refetch. It is the data half of the same server/client split described above: the rpc contract (pure Schema) is shared, while its handler lives in a server-only Layer the client never imports.
import { Boundary, h } from "@weftui/core";
import { Stream } from "effect";
import { GetStock } from "./data/inventory";
const StockPanel = (productId: number) =>
Boundary.rpc(
GetStock,
() => ({ id: productId }), // a fresh typed payload per call (SSR / refetch / mount)
(resource) =>
h.p([
"in stock: ",
h.span([Stream.map(resource.value.changes, (stock) => String(stock.units))]),
h.button({ type: "button", onclick: () => resource.refetch }, "Refresh"),
]),
{ fallback: h.p("loading stock…") }, // shown only on a client-first SPA mount
);Under SSR the server resolves the rpc in-process, successSchema-encodes the result inline as <script type="application/json">, and renders in place; hydrate reads that payload positionally, seeds the Resource, and adopts the DOM without re-calling the rpc (replay, never retry). The full model — the contract/handler split, router wiring, the four lifecycles, the Resource handle, and typed-failure replay — lives in one place: the RPC Data Boundaries guide. This page does not repeat it.
Note.
Boundary.rpcresolves through the ambientAppRpcClientTagseam, which@weftui/routerprovides on both sides. In a router-less mount there is no seam, so the boundary resolves to a descriptive "needs router/rpc" error (not a defect).
When to use
Boundary.rpc— data that must be resolved on the server (behind a server-only service, credential, or private network) and rendered into the initial HTML, then refreshable on the client (refetch / client-first SPA mount) over the same rpc.Boundary.suspend— async data that loads on the client (or streams the shell then fills); see the Boundary API.
See also
- rpc data boundaries guide — the full
Boundary.rpcwalkthrough: contract/handler split, router wiring, the four lifecycles, and typed-failure replay - Routing —
@weftui/routerbuilds on this SSR + hydration model for full-page nested routing Boundary.rpcAPI referenceServerTagAPI reference- examples/router-ssr — a runnable shop with an SSR-replayed, refetchable live-stock
Boundary.rpc - examples/ssr-hydration — SSR + hydration without server data loading