@mounted
Runs once per component, after that component has been built and committed.
In the browser that is the first moment it can reach the real page — focus an input, measure an element, hand a node to a library that wants one. On the server, where there is no page to reach, it is the moment a component is finished enough to fetch what it needs. Both are below; the browser one is the one to meet first.
The situation it is for
A dialog that opens with the cursor already in its first field. Nothing about that can be decided
while rendering — the input does not exist yet, and focus() on nothing does nothing:
import { createRef } from "@ramonda/core";
class RenameDialog extends Component {
private box = createRef<HTMLInputElement>();
@mounted
takeFocus() {
this.box.current?.focus();
}
render() {
return (
<label>
New name
<input ref={this.box} />
</label>
);
}
}
The ref is empty while the render runs and holds the element by the time @mounted does. That gap
is the whole reason this moment exists.
Children mount before their parent. By the time a parent's @mounted runs, everything inside it
is already on the page — so a parent measuring its children finds them there.
It runs on the SERVER too
This is the half of @mounted that is easy to miss, and the one most worth knowing. A server render
has no DOM and no page — but it still builds every component, and a shared @mounted runs there
as well as in the browser. env is what chooses:
@mounted
both() {} // server AND client — the default
@mounted({ env: "client" })
onlyHere() {} // the browser only: focus, measure, reach the page
The method is handed the side it is on, so one can branch rather than two declaring opposites.
Returning a promise makes the server wait
An async @mounted is awaited on the server — which is how a page is rendered with its data
already in it, and there is no separate API for it:
class Profile extends Component<{ id: string }> {
@state user: { name: string } | null = null;
@mounted
async load() {
if (this.user) return; // the server already fetched it
this.user = await getUser(this.props.id);
}
render() {
return <article>{this.user?.name ?? "…"}</article>;
}
}
On the client the same promise is fire-and-forget: a live page should paint before the data arrives, so nothing waits.
That if is the one line the shape needs. The method runs on both sides, so without it the browser
would fetch again — and it does not need a framework flag, because whatever the server fetched is in
@state, travels in the hydration blob, and is restored before any client lifecycle runs. Only
the component knows what "already fetched" means.
See Async work on the server for the whole picture.
Anything reaching the page belongs in env: "client"
The example at the top of this page has no env and does not need one: this.box.current is
null on the server, so ?.focus() does nothing there. Where a method would throw on the
server rather than quietly do nothing — anything touching window, document or a measurement —
say env: "client" and it is never called there at all.
More than one, and on a hook
A class may declare several, and they run in the order they are written. @mounted also works
on a hook, where it runs when the component holding the hook mounts.
What it refuses
Anything but a method.
What it costs, and when not to reach for it
It runs after the DOM is committed, so anything it writes to state causes a second render. That
is correct for a measurement — you cannot know a width before there is a box — and wasteful for a
value you could have derived. If the answer does not need the page,
@created or a @compute gets it
without the extra pass.
An async one that rejects with nothing to catch it is reported as
RMD059.