Skip to content

Server-Side Rendering

renderToString() renders a Sygnal component to an HTML string without a browser DOM. Use it for server-rendered pages, static site generation, or any environment where you need HTML output from your components.

import { renderToString } from 'sygnal'
const html = renderToString(App, { state: { count: 0 } })
// → '<div class="counter"><h1>Count: 0</h1><button>+</button></div>'

Pass a component function and optional state:

function Greeting({ state }) {
return <div className="greeting">Hello, {state.name}!</div>
}
Greeting.initialState = { name: 'World' }
// Uses component's initialState
renderToString(Greeting)
// → '<div class="greeting">Hello, World!</div>'
// Override state
renderToString(Greeting, { state: { name: 'Alice' } })
// → '<div class="greeting">Hello, Alice!</div>'

Sub-components are rendered recursively. State lensing works the same as in the browser — pass state="propName" to scope child state:

function App({ state }) {
return (
<div>
<Header state="header" />
<Content state="content" />
</div>
)
}
App.initialState = {
header: { title: 'My App' },
content: { body: 'Welcome' },
}
renderToString(App)
// Renders both Header and Content with their scoped state

Collections render each item from the state array:

function TodoItem({ state }) {
return <li>{state.text}</li>
}
function TodoList({ state }) {
return (
<ul>
<Collection of={TodoItem} from="items" />
</ul>
)
}
TodoList.initialState = {
items: [
{ id: 1, text: 'Buy milk' },
{ id: 2, text: 'Write docs' },
],
}
renderToString(TodoList)
// → '<ul><div><li>Buy milk</li><li>Write docs</li></div></ul>'

Component context is computed from state and propagated to descendants:

function App({ state, context }) {
return <div className={`theme-${context.theme}`}>{state.label}</div>
}
App.initialState = { label: 'Hello', darkMode: true }
App.context = { theme: (state) => state.darkMode ? 'dark' : 'light' }
renderToString(App)
// → '<div class="theme-dark">Hello</div>'

Error boundaries work during SSR. Components with onError render fallback content; those without render an empty <div data-sygnal-error>:

function Fragile({ state }) {
throw new Error('Oops')
}
Fragile.onError = (err, { componentName }) => (
<div className="error">Something went wrong in {componentName}</div>
)
renderToString(Fragile)
// → '<div class="error">Something went wrong in Fragile</div>'
ComponentSSR Behavior
PortalChildren rendered inline (no target container on server)
TransitionUnwrapped to child element (no animation)
SuspenseAlways renders children (not fallback)
SlotUnwrapped to children
CollectionItems rendered from state array
SwitchableActive component rendered based on state

Embed serialized state in a <script> tag for client-side rehydration:

const html = renderToString(App, {
state: { count: 5 },
hydrateState: true,
})
// Appends: <script>window.__SYGNAL_STATE__={"count":5}</script>

Use a custom variable name:

renderToString(App, {
state: { count: 5 },
hydrateState: '__MY_APP_STATE__',
})
// Appends: <script>window.__MY_APP_STATE__={"count":5}</script>

On the client, read the embedded state to hydrate:

import { run } from 'sygnal'
const initialState = window.__SYGNAL_STATE__ || App.initialState
run(App, '#app', { initialState })

The Astro server renderer uses renderToString internally. When using the Sygnal Astro integration, SSR happens automatically:

---
import Counter from '../components/Counter.jsx'
---
<Counter client:load />
function renderToString(
component: ComponentFunction,
options?: RenderToStringOptions
): string
OptionTypeDefaultDescription
stateanyComponent’s .initialStateState for the root component
propsRecord<string, any>{}Props to pass to the component
contextRecord<string, any>{}Parent context to merge with
hydrateStateboolean | stringEmbed state in <script> tag
  • Intent and Model are skipped — SSR is render-only. Event handlers, streams, and state reducers don’t run on the server.
  • Refs are not populated — No DOM exists, so createRef() objects remain { current: null }.
  • Lazy componentslazy() wrappers render their loading placeholder. For SSR, import components directly instead.
  • Factory components — Components created via the component() factory (with isSygnalComponent) render a placeholder <div> since the view function can’t be extracted from the wrapped factory.