Server-Side Rendering
Flexium renders to HTML on the server, then hydrates in the browser. Per-request state is isolated via AsyncLocalStorage — concurrent requests don't leak signals.
Basic SSR
Server entry (Node, Bun, Deno, edge runtime):
import { renderToString } from 'flexium/server'
import App from './App'
const html = await renderToString(<App />)
response.send(`
<!DOCTYPE html>
<html>
<head><title>My app</title></head>
<body>
<div id="app">${html}</div>
<script type="module" src="/client.js"></script>
</body>
</html>
`)Client entry:
import { hydrate } from 'flexium/dom'
import App from './App'
hydrate(<App />, document.getElementById('app')!)hydrate attaches signals to the existing DOM without re-rendering. Event handlers light up; subsequent updates flow normally.
Streaming SSR
renderToString blocks until all Suspense boundaries resolve. For better TTFB, use the streaming primitives:
import { renderToStream } from 'flexium/server'
const stream = renderToStream(<App />)
response.setHeader('content-type', 'text/html')
response.setHeader('transfer-encoding', 'chunked')
for await (const chunk of stream) {
response.write(chunk)
}
response.end()The head + above-fold flushes immediately. Each Suspense boundary streams in as its data resolves.
Per-request isolation
Per-request signals (auth user, locale, request context) need isolation. Flexium uses AsyncLocalStorage internally — but for app-level state, structure it explicitly:
// server.ts
import { runWithRequest } from 'flexium/server'
async function handler(req, res) {
await runWithRequest({ user: await getUser(req) }, async () => {
const html = await renderToString(<App />)
res.send(html)
})
}// App.tsx
import { useRequest } from 'flexium/server'
function Header() {
const { user } = useRequest()
return <p>Hello, {user?.name ?? 'guest'}</p>
}Two concurrent requests with different users see isolated state. No globals, no shared mutable references.
CSS extraction
When using flexium/css, extracted styles need to ship with the HTML:
import { renderToString, getStyleTag, resetStyles } from 'flexium/server'
async function handler(req, res) {
resetStyles() // reset per request
const html = await renderToString(<App />)
const styles = getStyleTag() // <style>...</style>
res.send(`<!DOCTYPE html>
<html>
<head>${styles}</head>
<body><div id="app">${html}</div></body>
</html>`)
}In production with vite-plugin-flexium's build-time extraction, this happens at compile time and you serve a regular CSS file instead — no runtime collection needed.
Edge runtimes
renderToString works in Cloudflare Workers, Vercel Edge Functions, Deno Deploy, Bun. Just import flexium/server and use the same APIs.
For full-stack framework features (file-based routing, server loaders, Stream-based realtime, SSP), see Flexism — built on top of Flexium for fullstack apps.
Hydration mismatches
If server-rendered HTML diverges from what the client expects, hydration logs a warning and falls back to client-side render of the divergent subtree. Common causes:
- Reading
Date.now()orMath.random()at render time (different on server vs client) - Using
window/documentduring render - Time-zone-dependent date formatting
Fix: do these in unsafeEffect (client-only) or compute on the server and pass as a prop.
Next
→ Examples — working code for everything covered in the guides.