# Server Rendering

`elements-kit/server` renders the same JSX you write for the browser to an HTML stream — in Node, Cloudflare Workers, or any JavaScript runtime, with no DOM. `elements-kit/hydrate` then adopts that HTML on the client and makes it interactive without rebuilding it.

## Rendering on the server

Pass a **thunk**, not evaluated JSX — JSX evaluates eagerly, so the server renderer must install before the first jsx call runs.

```tsx
import { renderToString } from "elements-kit/server";

const html = await renderToString(() => <App />);
```

For streaming responses use `renderToStream`, which returns a standard `ReadableStream<Uint8Array>`:

```tsx
import { renderToStream } from "elements-kit/server";

export default {
  fetch: () =>
    new Response(renderToStream(() => <App />), {
      headers: { "content-type": "text/html" },
    }),
};
```

### Snapshot semantics

A server render is a one-shot snapshot of your reactive state:

* Signals and computeds are read **once**, without subscribing.
* **Effects do not run** — they are client-only.
* `on:` handlers and `ref` callbacks are skipped; interactivity attaches at hydration.
* `innerHTML` throws — there is no raw HTML sink. Text and attributes are escaped.

### Async data

`promise` and `async` reactive values used as children become **async insertion points**: everything before them flushes immediately, then the stream awaits the value and continues.

```tsx
import { promise } from "elements-kit/utilities/promise";

const user = promise(fetch("/api/user").then((r) => r.json()));

const App = () => (
  <main>
    <h1>Profile</h1>          {/* flushes immediately */}
    <p>{user}</p>             {/* stream awaits, then continues */}
  </main>
);
```

Resolved values are also serialized into a `<script type="application/json" id="ek-data">` tag at the end of the stream. On the client, `hydrate` seeds pending `promise`/`async` values from this data — the server value displays immediately, and the real client-side settlement overwrites it when it arrives (stale-while-revalidate). Rejected values abort the stream — errors propagate, never swallowed.

**Prefer `async` + `.run()` for server-rendered data.** `run()` calls made while hydration evaluates the tree are deferred; when the value was seeded from ek-data, the fetcher is never invoked — no duplicate request:

```tsx
import { async } from "elements-kit/utilities/async";

const user = async(() => fetch("/api/user").then((r) => r.json()));

const App = () => {
  user.run(); // server: awaited + serialized · hydration: skipped when seeded
  return <p>{user}</p>;
};
```

`promise(fetch(...))` cannot skip the request — the fetch fires before the library ever sees it — and `.start()` always executes, because reactive re-runs need the first execution to collect dependencies.

## Hydrating on the client

```tsx
import { hydrate } from "elements-kit/hydrate";

const { dispose } = hydrate(document.getElementById("app")!, () => <App />);
```

Hydration **re-executes** the component tree in claim mode: instead of creating DOM, the runtime walks the server-rendered nodes and adopts them —

* static elements and text keep their identity (no rebuild, no flicker),
* `on:` handlers attach to the claimed nodes,
* dynamic children (`{() => sig()}`) bind live to the server-emitted comment markers,
* `<For>` adopts each keyed row and reconciles later updates in place,
* async children keep their server-rendered content visible until the client value settles.

There is no compiler and no serialized closures — your handlers are ordinary closures recreated by re-execution.

### Matching trees

The server and client must render the same tree. If a node doesn't match, that subtree is discarded and rendered fresh; pass `onMismatch` to observe it:

```tsx
hydrate(container, () => <App />, {
  onMismatch: ({ expected, found }) => report(expected, found),
});
```

## Raw HTML — `<Fragment html>`

The one deliberate raw-HTML sink in the library: pass `html` on a fragment and the child — a `MaybeReactive<string>` — renders as markup instead of text:

```tsx
const markdown = signal("<h2>rendered</h2><p>from a CMS</p>");

<article>
  <Fragment html>{markdown}</Fragment>
</article>
```

The region lives between slot markers, so it server-renders, hydrates (server content is kept until the source changes), and re-renders when the signal updates. Parsing is **script-inert** — `<script>` tags in the string never execute.

### Foreign content — `ns`

The client parses the string in a detached `<template>`, which is always HTML. The parser only enters SVG or MathML mode when it *sees* an `<svg>` / `<math>` start tag, so an interior fragment on its own gets neither — a bare `<path/>` lands in the XHTML namespace, sits in the DOM with the right tag name and attributes, and paints nothing.

`ns` marks the string as interior content of one of those subtrees:

```tsx
<svg viewBox="0 0 24 24">
  <Fragment html ns="svg">{'<path d="M0 0"/>'}</Fragment>
</svg>
```

The markup is parsed against a detached root element of that namespace and the root is then dropped, so only the interior reaches the DOM. Because the element itself is the parser's context — not an `<svg>…</svg>` wrapper pasted around the string — markup like `"</svg><p>"` has nothing to close out of. Accepts `"svg"` or `"mathml"`; the server renderer ignores it, since the string is already inside its parent there.

Generated code is the expected caller — a compiler knows the namespace at build time, which is how [the SVG plugin](/integrations/svg) uses it. Reach for it by hand only when you are handing raw markup to an `<svg>` or `<math>` parent yourself.

**Sanitize untrusted input at the call site.** The string is emitted verbatim on the server and parsed as markup on the client; attribute-based XSS (`onerror=…`, `javascript:` URLs) is your responsibility to strip. Everywhere else strings remain text-only, and the `innerHTML` prop still throws.

## Lazy loading & Await

Code splitting composes from primitives you already have — `async(() => import("./chart"))` — and `<Await>` from `elements-kit/await` (the Suspense equivalent) adds the loading boundary: the server stream awaits dynamic imports in order (the fallback never server-renders), the client shows the fallback until the chunk lands, and hydration keeps the server content visible. See the [Astro page](/integrations/astro#lazy-loading--await) for a live demo.

## Astro

Using Astro? The [Astro integration](/integrations/astro) packages this renderer pair as an island framework — `<Counter client:load />` server-renders through `renderToStream` and hydrates through `hydrate`, with no manual wiring.

## Not in v1

* Custom elements / Declarative Shadow DOM rendering.
* Class components other than `For`.
* Out-of-order (Suspense-style) streaming.
* Qwik-style resumability — hydration re-executes component code by design.
