Server-Side Rendering
Render on the server, transfer state explicitly, and hydrate without re-rendering.
Rendering Model
Overview
Xote's SSR story is built from three modules that line up with the render flow:
- SSR - render nodes to HTML
- SSRState - serialize state on the server and restore it on the client
- Hydration - attach reactivity and event handlers to existing DOM
The key idea is that the client should not re-render what the server already produced. Hydration walks the server HTML, reconnects reactive boundaries, and continues from there.
Render on the Server
Use SSR.renderToString for fragments and SSR.renderToStringWithRoot when you want a hydration root.
1open Xote23let app = () => {4 <div>5 <h1> {Node.text("Hello from the server")} </h1>6 </div>7}89let html = SSR.renderToStringWithRoot(app, ~rootId="root")Full Document Rendering
Use SSR.renderDocument when the server is responsible for the whole HTML document.
1let html = SSR.renderDocument(2 ~head="<title>Xote App</title>",3 ~scripts=["/client.js"],4 ~stateScript=SSRState.generateScript(),5 app,6)Environment Detection
Use SSRContext when code needs to branch between server and client behavior.
1let greeting = SSRContext.match(2 ~server=() => "Rendered on the server",3 ~client=() => "Rendered on the client",4)State and Hydration
State Transfer
If the server and client must start from the same state, register signals with SSRState.
Creating Synced State
1let count = SSRState.make("count", 0, SSRState.Codec.int)2let name = SSRState.make("name", "Ada", SSRState.Codec.string)Syncing Existing Signals
1let draft = Signal.make("")2SSRState.sync("draft", draft, SSRState.Codec.string)Built-in Codecs
Use the built-in codecs for primitives and common containers, or create your own with SSRState.Codec.make.
1SSRState.Codec.int2SSRState.Codec.string3SSRState.Codec.bool4SSRState.Codec.array(SSRState.Codec.string)5SSRState.Codec.option(SSRState.Codec.int)6SSRState.Codec.tuple2(SSRState.Codec.int, SSRState.Codec.string)On long-lived servers that render more than one request, call SSRState.clear() between renders to reset the registry.
Client-Side Hydration
Hydration connects the client runtime to server-rendered DOM without replacing it.
1Hydration.hydrateById(app, "root", ~options={2 onHydrated: () => Console.log("hydrated"),3})In Practice
Complete Example
A typical setup shares the same component between server and client.
Shared Component
1open Xote23let count = SSRState.make("count", 0, SSRState.Codec.int)45let app = () => {6 <div>7 <h1>8 {Node.signalText(() => "Count: " ++ Int.toString(Signal.get(count)))}9 </h1>10 <button onClick={_ => Signal.update(count, n => n + 1)}>11 {Node.text("Increment")}12 </button>13 </div>14}Server Entry
1Router.initSSR(~pathname="/", ())23let html = SSR.renderDocument(4 ~scripts=["/client.js"],5 ~stateScript=SSRState.generateScript(),6 app,7)Client Entry
1Router.init()2Hydration.hydrateById(app, "root")Hydration Markers
Xote inserts HTML comments around reactive boundaries during SSR. The client uses those markers to find signal text, fragments, keyed lists, and lazy components while hydrating.
Working Style
Best Practices
- Render the same component tree on the server and the client so hydration can attach cleanly.
- Initialize the router for the right environment:
initSSRon the server,initon the client. - Clear
SSRStatebetween server renders, especially in custom or long-lived processes. - Choose codecs deliberately because they define the contract between serialized output and restored client state.
Next Steps
- Read Router if these pages also depend on route state.
- Read the Technical Overview if you want the internal model behind hydration and runtime ownership.