[]previous and next page

Portals on the server

A Portal renders nothing where it is written, so a dialog or sheet that is open at render time would be missing from the HTML.

For agentsUpdated

A Portal renders nothing where it is written, so a dialog or sheet that is open at render time would be missing from the HTML. Give the render a collector and emit it at the end of <body>:

src/entry-server.tsxtsx
import { createServerPortals, renderToStringAsync } from "@shadow-js/web/server";

const portals = createServerPortals();
const { html, data } = await renderToStringAsync(() => <App />, { portals });
const page =
  `<!doctype html><html><body><div id="app">${html}</div>` +
  `${portals.html()}</body></html>`;
  • The content is server-rendered: role="dialog" markup is in the document, and the client adopts it on hydration instead of creating a second copy, so there is no flash. The same holds for renderToString and renderToStream (for a stream, read portals.html() after the stream has ended).
  • Each portal is one block, in render order. Nested portals are blocks of their own, and a render produces at most 256 of them.
  • @shadow-js/dusk passes a collector and emits the blocks at the end of <body> in every page mode, so a Dusk page needs no code for this.
  • A portal whose mount is an element other than body is placed at the end of body by the server (the server does not know your element) and moved into the mount during hydration. Until then it shows at the end of body. Keep such a dialog closed at render time if that matters, or mount into body.
  • Portals with useShadow are not server-rendered; the browser creates them.

The contract, with the exact block format and the hydration rules, is in docs/contracts/ssr.md ("Portals").