Vite SSR with SolidJS
Set up server-side rendering (SSR) with SolidJS, Vite, and Nitro. This setup uses renderToStringAsync for HTML generation and supports client hydration.
├── src/
│ ├── app.tsx
│ ├── entry-client.tsx
│ ├── entry-server.tsx
│ └── styles.css
├── package.json
├── README.md
├── tsconfig.json
└── vite.config.mjs
Overview
Add the Nitro Vite plugin to your Vite config
Configure client and server entry points
Create a server entry that renders your app to HTML
Create a client entry that hydrates the server-rendered HTML
1. Configure Vite
Add the Nitro and SolidJS plugins to your Vite config. SolidJS requires explicit JSX configuration and both ssr and client environments:
import solid from "vite-plugin-solid";
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";
export default defineConfig({
plugins: [solid({ ssr: true }), nitro()],
esbuild: { jsx: "preserve", jsxImportSource: "solid-js" },
environments: {
ssr: {
build: { rollupOptions: { input: "./src/entry-server.tsx" } },
},
client: {
build: { rollupOptions: { input: "./src/entry-client.tsx" } },
},
},
});
Enable SSR mode in the Solid plugin with solid({ ssr: true }). Configure esbuild to preserve JSX for Solid's compiler and use Solid's JSX runtime. SolidJS requires explicit ssr and client environment configuration in Vite.
2. Create the App Component
Create a shared SolidJS component using reactive signals:
import { createSignal } from "solid-js";
export function App() {
const [count, setCount] = createSignal(0);
return (
<div>
<h1>Hello, Solid!</h1>
<button onClick={() => setCount((count) => count + 1)}>Count: {count()}</button>
</div>
);
}
SolidJS uses signals (createSignal) for state management. Unlike React's useState, signals are getter functions that you call to read the value.
3. Create the Server Entry
The server entry renders your SolidJS app to HTML using renderToStringAsync and includes the HydrationScript for client-side hydration:
import { renderToStringAsync, HydrationScript } from "solid-js/web";
import { App } from "./app.jsx";
import clientAssets from "./entry-client?assets=client";
import serverAssets from "./entry-server?assets=ssr";
export default {
async fetch(req: Request): Promise<Response> {
const appHTML = await renderToStringAsync(() => <App />);
const rootHTML = await renderToStringAsync(() => <Root appHTML={appHTML} />);
return new Response(rootHTML, {
headers: { "Content-Type": "text/html" },
});
},
};
function Root(props: { appHTML?: string }) {
const assets = clientAssets.merge(serverAssets);
return (
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{assets.css.map((attr: any) => (
<link key={attr.href} rel="stylesheet" {...attr} />
))}
{assets.js.map((attr: any) => (
<link key={attr.href} type="modulepreload" {...attr} />
))}
</head>
<body>
<div id="app" innerHTML={props.appHTML || ""} />
<HydrationScript />
<script type="module" src={assets.entry} />
</body>
</html>
);
}
SolidJS requires rendering the app separately from the shell (two-phase rendering). The app HTML is injected via innerHTML to preserve hydration markers. Include the HydrationScript component to inject the script Solid needs to rehydrate on the client. Import assets using the ?assets=client and ?assets=ssr query parameters to collect CSS and JS from each entry point.
4. Create the Client Entry
The client entry hydrates the server-rendered HTML, restoring Solid's reactivity:
import { hydrate } from "solid-js/web";
import "./styles.css";
import { App } from "./app.jsx";
hydrate(() => <App />, document.querySelector("#app")!);
The hydrate function attaches Solid's reactive system to the existing server-rendered DOM inside #app. The component is wrapped in a function () => <App /> as required by Solid's API.