Getting Started: SSR
SSR mode renders your root custom element on the server as Declarative Shadow DOM. The client entry then imports the same component modules and hydrates the existing DOM instead of replacing the whole document.
Installation
bash
pnpm add @dathra/core @dathra/components @dathra/runtime @dathra/plugin1. vite.config.ts
ts
import { dathraVitePlugin } from "@dathra/plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
dathraVitePlugin({
mode: "ssr",
ssr: {
entry: "/src/entry-server.tsx",
},
}),
],
});2. tsconfig.json
json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@dathra/core"
}
}3. src/Counter.tsx
tsx
import { defineComponent, css } from "@dathra/components";
import { signal, computed } from "@dathra/core";
const Counter = defineComponent(
"my-counter",
({ props }) => {
const count = signal(props.initial.value);
const doubled = computed(() => count.value * 2);
return (
<div>
<p>Count: {count.value}</p>
<p>Doubled: {doubled.value}</p>
<button onClick={() => count.set(count.value + 1)}>+1</button>
</div>
);
},
{
props: {
initial: { type: Number, default: 0 },
},
styles: [css`
:host { display: block; padding: 16px; }
button { border-radius: 8px; }
`],
},
);
export { Counter };Count: {count.value}
Doubled: {doubled.value}
4. src/AppRoot.tsx
tsx
import { defineComponent } from "@dathra/components";
import "./Counter";
const AppRoot = defineComponent(
"app-root",
() => {
return (
<main>
<h1>Dathra SSR App</h1>
<my-counter initial="5"></my-counter>
</main>
);
},
);
export { AppRoot };Dathra SSR App
5. index.html
html
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/entry-client.ts"></script>6. src/entry-server.tsx
tsx
import { defineSsrEntry, render } from "@dathra/core/ssr";
import { AppRoot } from "./AppRoot";
const handler = defineSsrEntry(async () => {
return {
html: render(AppRoot),
};
});
export default handler;7. src/entry-client.ts
ts
import { hydrate } from "@dathra/core/hydration";
void import("./AppRoot").then(() => {
queueMicrotask(() => {
hydrate(document);
});
});In this flow, the server emits<app-root>with Declarative Shadow DOM. The
browser upgrades and hydrates the app root, and nested components like <my-counter>come along as part of the same tree.
Passing Request State
Pass route, locale, or other request-derived values as component attrs. This docs app uses
the same pattern forroutePath:
tsx
const handler = defineSsrEntry(async ({ request }) => {
const routePath = new URL(request.url).pathname;
return {
html: render(AppRoot, { routePath }),
};
});Common Mistakes
- Register the same component modules on both the server and the client. The server needs them to render DSD; the client needs them to upgrade and hydrate custom elements.
- Call
hydrate(document)after client imports have resolved. - Keep request-specific state in the SSR entry or a per-request store; do not rely on module globals for request data.