Ramonda

Client, server, shared

When you render on both the server and the browser, some setup should run on both, some only in the browser, and some only on the server. Every lifecycle decorator takes an env to say which:

@created
init() {} // "shared" — both sides (the default)

@created({ env: "client" })
startPolling() {} // only in the browser

@created({ env: "server" })
stampBuildTime() {} // only during a server render
server renderbrowser
"shared" (default)
"client"
"server"

(Subscriptions — @interval, timers, @onWindow, your own — have no env; they are always client-only. That is why a subscription never leaks into a server render. The Timeout and Interval hooks have no env either, for the same reason and by a different route: start does nothing during a server render and says so by returning false, so a method that starts one is safe to call from shared code.)

Knowing the side inside a shared method

Sometimes a shared method runs mostly the same both ways but must skip one step on one side — fetch on the client, not during the server render. Rather than split it in two, each lifecycle method receives its side as an argument:

import { RenderEnv } from "@ramonda/core";

@mounted
async load(env: RenderEnv) {
  if (env === "server") return;            // the client fetches after hydration
  this.data = await fetch(`/api/thing/${this.props.id}`).then((r) => r.json());
}

Read the argument, not typeof window: under server rendering the DOM is a shim, so window and document exist there too and the check would lie. env is "client" or "server", and it stays correct even inside an async method after an await.

env is not a security boundary

env: "server" chooses where code runs, not whether it ships. A "server" method's body is part of your component, so it is bundled and sent to the browser like everything else — someone can read it there even though it never executes. Never put a secret (an API key, a private token, logic you don't want seen) in a "server" lifecycle expecting the client can't reach it. If something must stay secret it lives behind an API the browser calls; that boundary is the server, not a decorator.

Which to use

  • "shared" for anything that produces the page — reading props, seeding state, fetching. It runs on the server so the result is in the HTML, and on the client so a client-side navigation gets the same result.
  • "client" for anything that touches the browserwindow, document, localStorage, timers, geolocation — or anything that must not happen twice (an analytics event).
  • "server" for anything only the server can know and shouldn't be recomputed — a build timestamp, a value read from a file. Pair it with @persist so it travels to the client.

prerendered at 2026-08-21 20:18:04 UTCclient @created ran 0 time(s) — the value above came from the server

That timestamp was computed by the server and restored here; reloading doesn't change it, because the client never recomputes it.

The rule of thumb

Anything with a matching teardown should be client-only. A window.addEventListener (or a counter you raise in @created and lower in @destroyed) has a setup and a cleanup — but a server render never unmounts, so its cleanup never runs. Run those on the client. (Both of these were real router bugs, from a lifecycle defaulting to shared when the work was client-only.)

Don't branch on the environment in render()

render() {
  return typeof window === "undefined" ? <Server /> : <Client />; // ✗
}

That produces different output on the two sides by design — exactly what RMD007 reports. Decide by side in the lifecycle, where env says so out loud.

Next