---
title: "JSX SSR"
description: "Server rendering and hydration rules for @ecopages/jsx."
group: SSR
order: 1
---
# JSX SSR
`@ecopages/jsx/server` is the dedicated SSR entrypoint for `renderToString(...)`.
It serializes the same JSX authoring output used by the DOM renderer.
## The Two SSR Modes
```tsx
/** @jsxImportSource @ecopages/jsx */
import { renderToString } from '@ecopages/jsx/server';
const view = ;
const html = renderToString(view);
const hydratedHtml = renderToString(view, { mode: 'hydrate' });
```
- `renderToString(view)` returns plain HTML
- `renderToString(view, { mode: 'hydrate' })` returns HTML plus binding markers that the client hydrator can reconnect later
Use `mode: 'hydrate'` only when the client will hydrate that exact same view.
## What Hydrated SSR Adds
Hydrated output embeds binding descriptors for dynamic parts such as:
- event bindings
- property bindings
- boolean and normal attribute bindings
Those markers let `hydrate(...)` attach listeners and property wiring without rebuilding the existing DOM tree.
## Client Attach Step
The normal client follow-up is:
```tsx
/** @jsxImportSource @ecopages/jsx */
import { createRoot } from '@ecopages/jsx';
function App() {
return ;
}
const container = document.querySelector('#app');
if (container instanceof HTMLElement) {
createRoot(container).hydrate();
}
```
If hydration markers are missing, the client runtime falls back to a normal render.
## SSR And Custom Elements
When JSX SSR encounters a registered Radiant custom element, the server custom-element render hook asks Radiant's **server pipeline** to serialize the host (same path as `renderRadiantElementHostToString` from `@ecopages/radiant/server/radiant-element-ssr`). Radiant Element Hosts do not expose a durable instance `renderHostToString()` API. That instance method remains only the generic third-party custom-element contract understood by `@ecopages/jsx/server`.
At SSR time the runtime:
1. instantiates the registered custom element constructor
2. applies normalized attributes and serialized children
3. serializes the host through the Radiant server pipeline (light-DOM only)
Event handlers are not serialized into HTML. They reconnect only during client hydration.
See [Component SSR](/docs/ssr/component-ssr) for Radiant adapter helpers (`renderComponent`, install runtime, light-DOM policy).
## Server Rendering Rules
- `on:*` and `on-native:*` bindings are skipped in raw HTML output
- `prop:*` bindings are applied as properties on server-renderable custom element instances
- plain text content is escaped
- iterables are serialized by walking their children in order
- subscribable and signal-like values are snapshotted to their current value during SSR
## Async SSR Pattern
Async work belongs before the render pass starts, not inside it.
- fetch data, resolve assets, or build view models before calling the SSR helper
- pass resolved values into the component or controller before `renderToString(...)` runs
- keep `render()` synchronous and treat SSR as a snapshot of current state
For example, this is the intended shape:
```ts
const product = await api.getProduct(productId);
const rendered = await renderController(ProductController, {
tagName: 'section',
initialize: (controller) => {
controller.product = product;
},
});
```
By contrast, fetching inside `render()` is not part of the current SSR model.
Orchestration **around** SSR may still be async on Node. For example, a route handler can `await` data loading, then call `renderToString(...)` inside `withActiveSsrScopeValue(...)`. Keep component `render()` synchronous; treat SSR as a snapshot of resolved state.
## SSR Render Scope
`renderToString(...)` runs inside an active SSR render scope. `@ecopages/jsx/server` is Node-only and stores that scope in `AsyncLocalStorage`, so concurrent requests and abandoned async work do not leak state across unrelated renders.
Most app code never touches scope directly. Framework adapters use it to carry shared server state across nested `renderToString(...)` calls or split entrypoints.
### When to use scope helpers
| Need | API |
| --- | --- |
| Share hydrate binding indexes across sibling `renderToString(...)` calls | `createServerHydrationBindingState()` + `withServerHydrationBindingState(...)` |
| Share other framework-owned SSR state (asset frames, runtime bridges, etc.) | `withActiveSsrScopeValue(...)` + `getActiveSsrScopeValue(...)` |
Prefer `Symbol.for('@your-package.namespace')` keys so scope values survive entrypoint boundaries. Await I/O outside the scope when you only need scoped state during a synchronous render snapshot.
### Example: sibling renders in one hydration root
```ts
/** @jsxImportSource @ecopages/jsx */
import { jsx } from '@ecopages/jsx/jsx-runtime';
import { renderToString, withActiveSsrScopeValue } from '@ecopages/jsx/server';
const SHARED_SCOPE_KEY = Symbol.for('@acme/ssr.shared-root');
const html = withActiveSsrScopeValue(SHARED_SCOPE_KEY, { started: true }, () => {
const page = renderToString(jsx('section', { class: 'page', children: 'Page' }), { mode: 'hydrate' });
const shell = renderToString(jsx('main', { class: 'layout', children: page }), { mode: 'hydrate' });
return shell;
});
```
Hydration binding indexes continue across both `renderToString(...)` calls because they share one active SSR scope.
## Mental Model
The important distinction is:
- SSR is HTML generation
- hydration is the later client attach step
They are related, but not the same operation.
## Related APIs
- [JSX Client Rendering](/docs/jsx/rendering) for `createRoot(...)`, `hydrate(...)`, and `hasHydrationMarkers(...)`
- [RadiantElement](/docs/components/radiant-element) for host-owned render lifecycle
- [Component SSR](/docs/ssr/component-ssr) for `renderComponent(...)` and the server pipeline
- [Hydration](/docs/ssr/hydration) for the current explicit client hydrator contract used by render-owning `RadiantElement` SSR pages