[]previous and next page

Server rendering

The two halves of Solid's rendering API live in different packages: render and hydrate in @shadow-js/web, the string and stream renderers in…

For agentsUpdated

The two halves of Solid's rendering API live in different packages: render and hydrate in @shadow-js/web, the string and stream renderers in @shadow-js/web/server.

Solid ShadowJS What differs
renderToString(fn) renderToString(fn, { renderId, head }) from @shadow-js/web/server Synchronous. A pending Suspense renders its fallback. Effects never run. No nonce option (no inline script is emitted).
renderToStringAsync(fn) renderToStringAsync(fn, { renderId, head, timeoutMs, maxPasses }) Resolves to { html, data }, not a string. data holds the resolved resources for the client. Renders in passes until nothing is pending (default 10 passes, 10 s).
renderToStream(fn) with pipe and pipeTo renderToStream(fn, { renderId, head, nonce, timeoutMs, signal, onShellReady }) Returns a ReadableStream<Uint8Array>; there is no pipe/pipeTo. The shell is sent first, then one chunk per resolved Suspense boundary, out of order. nonce goes on the swap script.
hydrate(fn, el, { renderId, owner }) hydrate(fn, el, { renderId, data, owner }) renderId must match the server's; data is the data of renderToStringAsync; owner creates the hydrated root under that owner so its providers are visible.
<HydrationScript />, generateHydrationScript HydrationScript, generateHydrationScript (server entry) Records early click and input events; hydrate() replays them.
getRequestEvent, isServer isServer from @shadow-js/core or @shadow-js/web There is no request event.
src/ssr-page.tsxtsx
import { useResource } from "@shadow-js/core";
import { Suspense } from "@shadow-js/web";
import {
  createComponent,
  renderToStream,
  renderToString,
  renderToStringAsync,
} from "@shadow-js/web/server";

function Page() {
  const post = useResource(async () => "Hello from the server");
  return (
    <main>
      <Suspense fallback={<p>loading</p>}>
        <h1>{post()}</h1>
      </Suspense>
    </main>
  );
}

const app = () => createComponent(Page, {});

const sync = renderToString(app);
console.log(`fallback in sync output: ${sync.includes("loading")}`);

const result = await renderToStringAsync(app);
console.log(`async html has data: ${result.html.includes("Hello from the server")}`);
console.log(`async data keys: ${Object.keys(result.data).length}`);

const decoder = new TextDecoder();
const chunks: string[] = [];
const reader = renderToStream(app).getReader();
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  chunks.push(decoder.decode(value));
}
console.log(`stream chunks: ${chunks.length}`);
console.log(`shell shows the fallback: ${(chunks[0] ?? "").includes("loading")}`);
console.log(`last chunk carries data: ${(chunks.at(-1) ?? "").includes("Hello from the server")}`);
Expected output
fallback in sync output: true
async html has data: true
async data keys: 1
stream chunks: 3
shell shows the fallback: true
last chunk carries data: true

Head tags are collected explicitly. Create a ServerHead, pass it as the head option, and write head.html() into the document <head>. This replaces MetaProvider plus getAssets(); without the option nothing is registered. textContent={expression} is escaped and rendered.

src/ssr-head.tsxtsx
import { Title } from "@shadow-js/web";
import { createServerHead, renderToString } from "@shadow-js/web/server";

const head = createServerHead();
const body = renderToString(() => <Title>Page</Title>, { head });
console.log(`title in the head collector: ${head.html().includes("<title")}`);
console.log(`title in the body markup: ${body.includes("<title")}`);

const text = "<b>";
console.log(`textContent expression: ${renderToString(() => <p textContent={text} />)}`);
Expected output
title in the head collector: true
title in the body markup: false
textContent expression: <p data-hk="0-0"><b></p>