Native Solid 2 fork. Original documentation and designs © WorkOS; historical posts retain their original context.

Server-side rendering

Render native Solid 2 primitives on the server and hydrate the same component tree on the client.

Overview

Server-side rendering produces HTML for each request. Static rendering uses the same server renderer at build time. These native components use solid-js and @solidjs/web version 2.0.0-rc.13, not React or a React framework. Compile the server and browser entries with the native Solid 2 compiler, and let your bundler select the packages’ default server exports and browser exports respectively.

Use the same application, initial props, and renderId for server rendering and hydration. Native identity generation then keeps IDs and ARIA relationships consistent; the old React-before-18 ID workaround does not apply.

A shared application

The following application is shared by both entries:

// your-app.tsx
import { Dialog } from '@radix-solid/primitives';
export default function MyApp() {
return <Dialog.Root>
<Dialog.Trigger>Edit profile</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Profile</Dialog.Title>
<Dialog.Description>Change your profile.</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>;
}

Server entry

Render the shared tree, include the native hydration script before the application markup, and load the built browser entry. clientEntry is the URL of that entry supplied by your bundler; escape it before inserting it into HTML.

import { escape, generateHydrationScript, renderToString } from '@solidjs/web';
import MyApp from './your-app.js';
export function renderHtml(clientEntry: string) {
const body = renderToString(() => <MyApp />, { renderId: 'app-' });
return `<!doctype html><html lang="en"><head><meta charset="utf-8">${generateHydrationScript()}</head><body><div id="app">${body}</div><script type="module" src="${escape(clientEntry, true)}"></script></body></html>`;
}

Import any required stylesheet through your bundler and include its emitted stylesheet links in the HTML shell, as you would for a client-rendered application.

Browser entry

Hydrate the existing mount instead of rendering a second application. Keep the identity prefix and initial component tree identical to the server entry.

import { hydrate } from '@solidjs/web';
import MyApp from './your-app.js';
const root = document.getElementById('app');
if (!root) throw new Error('Missing application mount.');
hydrate(() => <MyApp />, root, { renderId: 'app-' });

Portals and browser-only work

Native portals intentionally render no portal content on the server and mount it after the client settles. In this example, the dialog trigger is server-rendered, while the portal’s overlay and content are mounted on the client when the dialog opens. Do not invent server portal markup or use a legacy portal implementation to force a match.

Put browser-only work such as measuring elements or subscribing to browser events in native onSettled callbacks. Return a cleanup function from that callback so navigation or unmounting removes the subscription. Do not read window, document, or storage while rendering on the server.