What runs on the server
The server render builds the tree once, then disposes it.
The server render builds the tree once, then disposes it. Nothing is reactive there.
- Effects do not run:
useEffect,useRenderEffectandonMountare no-ops. Do browser work inonMount(it is skipped on the server by design) instead of guarding withtypeof window. - Memos and component bodies run, once. A signal read gives its current value; writes to signals do not re-render anything.
isServerfrom@shadow-js/coreistrue,isDevfollows the build.- Resources start their fetch.
renderToStringdoes not wait for them; their Suspense boundaries render the fallback.renderToStringAsyncandrenderToStreamwait (below). - Event handlers are dropped.
ref,use:andon:directives are dropped. - The root is disposed before
renderToStringreturns, soonCleanupcallbacks run. innerHTMLis written as raw markup; everything else is escaped. Attribute names are validated and URL attributes are sanitized exactly as on the client (Security).- Writes to signals created outside the render (module-level state) are allowed and are shared by every request in the process. Do not keep per-request data in module-level signals.
server-semantics.tsxtsx
import { isServer, onCleanup, onMount, useEffect, useMemo, useStore } from "@shadow-js/core";
import { renderToString } from "@shadow-js/web/server";
const record = { effects: 0, setup: 0, cleanups: 0 };
function Probe() {
const [n] = useStore(2);
const doubled = useMemo(() => n() * 2);
record.setup += 1;
useEffect(() => {
record.effects += 1;
});
onMount(() => {
record.effects += 1;
});
onCleanup(() => {
record.cleanups += 1;
});
return (
<strong>
{doubled()} {isServer ? "server" : "browser"}
{isServer ? null : <i>client only</i>}
</strong>
);
}
console.log(renderToString(() => <Probe />));
console.log(`effects: ${record.effects} setup: ${record.setup} cleanups: ${record.cleanups}`);
<strong data-hk=