[]previous and next page

The shortest round trip

One component, compiled twice from the same source: for the browser (default) and for the server (generate: "ssr", which @shadow-js/vite selects for SSR builds and…

For agentsUpdated

One component, compiled twice from the same source: for the browser (default) and for the server (generate: "ssr", which @shadow-js/vite selects for SSR builds and ssrLoadModule).

src/app.tsxtsx
import { useStore } from "@shadow-js/core";
import { For } from "@shadow-js/web";

export function App(props: { names: string[] }) {
  const [count, setCount] = useStore(0);
  return (
    <main>
      <h1>Hello {props.names[0]}</h1>
      <ul>
        <For each={props.names}>{(name) => <li>{name}</li>}</For>
      </ul>
      <button type="button" onClick={() => setCount((n) => n + 1)}>
        clicked {count()}
      </button>
    </main>
  );
}

export function Page() {
  return <App names={["Ada", "Grace"]} />;
}

On the server:

server.tsxtsx
import { renderToString } from "@shadow-js/web/server";
import { Page } from "./src/app";

console.log(renderToString(() => <Page />));
Expected output
<main data-hk=

Reading the output:

  • data-hk="0-0-0-0" is a hydration key on each template root: the outermost element of a JSX expression, which is the root of a component's markup and also the root of each piece of markup a callback returns (a For row, a Show branch). Elements nested in the same expression (h1, ul, button) carry no key; the client reaches them through their root.
  • <!--[--> ... <!--]--> wrap every dynamic slot ({props.names[0]}, {count()}, a list). They tell the client where a slot's content starts and ends.
  • The onClick handler is not in the markup. The server never serializes functions.
  • Text is escaped: & < > " '.

In the browser, hydrate takes the same component call and the container that holds this markup:

The rules this proves, which hold for every component:

  • hydrate(code, container, options?) claims elements by key instead of creating them. Compiled DOM code is the same as for client rendering; only template() behaves differently while hydrating: it returns the server's element for the next key.
  • Comment markers exist only in the server's HTML. hydrate removes them as it claims regions, and sweeps any it did not claim at the end. After hydration (and after any client render) the live DOM contains zero comment nodes.
  • hydrate returns a dispose function that stops the owner and removes the nodes. Calling it twice is safe.
  • A second hydrate while one is running throws; a pass is synchronous, so this only matters for re-entrant calls.