Lifecycle
Elur has two component models — functional (defineComponent) and class-based
(ElurComponent) — sharing the same lifecycle machinery. Because components run
once (they are not re-rendered), the lifecycle is simple: there's init,
mount, and unmount — no "update" phase. Reactivity is handled by
signals, not by re-running the component.
This page covers the class model in depth. For defineComponent — live props,
slots and ctx hooks — see Components.
# Lifecycle in functional components
With defineComponent, the same phases are exposed through ctx — registered
inside setup instead of as method overrides:
const Timer = defineComponent((_props, ctx) => {
const seconds = signal(0);
ctx.onMount(() => {
const id = setInterval(() => seconds.update((n) => n + 1), 1000);
return () => clearInterval(id); // cleanup on unmount
});
ctx.onUnmount(() => console.log("gone"));
ctx.onError((info) => console.error(info.cause));
ctx.onServerRender(() => { /* SSR only, never on client */ });
return html`<p>${() => seconds.value}s</p>`;
});ctx hook |
Class equivalent |
|---|---|
ctx.onMount(fn) |
onMount() — post-commit; may return a cleanup |
ctx.onUnmount(fn) |
onUnmount() |
ctx.onError(fn) |
onError(err) — receives ComponentErrorInfo |
ctx.onServerRender(fn) |
onServerRender() — SSR only |
| setup body | onInit() + render() |
The rest of this page documents the class model.
# The ElurComponent class
Every component extends ElurComponent and implements a render() method.
Lifecycle hooks are optional overrides:
import { ElurComponent, signal, html } from "@elurjs/core";
class Counter extends ElurComponent {
private count = signal(0);
override render() {
return html`
<button @click=${() => this.count.update((n) => n + 1)}>
${() => this.count.value}
</button>
`;
}
}
mount(new Counter(), "#app");# Lifecycle hooks
| Hook | When it runs | DOM available? | Can return |
|---|---|---|---|
onInit() |
Before render(), once |
No | — |
onServerRender() |
During SSR, after onInit() (never on client) |
No | — |
onMount() |
After the template is inserted into the DOM | Yes | A cleanup function |
onUnmount() |
Before the component is removed from the DOM | Yes | — |
onError(err) |
When onInit or onMount throws |
— | — |
# onInit()
Called once before render(). This is where you call provide() to register
context values and set up any state that render() depends on. There is no DOM
yet:
class App extends ElurComponent {
private theme = signal("dark");
override onInit() {
provide(ThemeKey, {
theme: this.theme,
toggle: () => this.theme.update((t) => t === "dark" ? "light" : "dark"),
});
}
override render() {
return html`<div>${this.children}</div>`;
}
}# onMount()
Called once after the rendered template is inserted into the DOM. This is where you access DOM elements (via refs), set up intervals, add event listeners, or start fetches. If you return a function, it is called as cleanup on unmount:
class Timer extends ElurComponent {
private seconds = signal(0);
override onMount() {
const id = setInterval(() => this.seconds.update((n) => n + 1), 1000);
return () => clearInterval(id); // cleanup on unmount
}
override render() {
return html`<p>Elapsed: ${() => this.seconds.value}s</p>`;
}
}The cleanup function returned from onMount is the equivalent of a separate
onCleanup hook. A standalone onCleanup(fn) is also exported — it registers
teardown on the current owner (useful inside setup or any owned scope).
# onUnmount()
Called right before the component is removed from the DOM. Use it for any
final cleanup that wasn't covered by the onMount return value:
class Modal extends ElurComponent {
override onMount() {
document.body.style.overflow = "hidden";
}
override onUnmount() {
document.body.style.overflow = "";
}
override render() {
return html`<div class="modal">${this.children}</div>`;
}
}# onError(err)
If onInit() or onMount() throws, onError(err) is invoked with the error.
If no onError is defined, the error propagates up. Use it to render a fallback
or log to an error service:
class RiskyComponent extends ElurComponent {
override onError(err: unknown) {
console.error("Component failed:", err);
}
override render() {
return html`<p>Content</p>`;
}
}# onServerRender()
A server-only hook that runs during SSR after onInit() and before render().
It never runs on the client. Use it for server-specific setup like seeding
initial data or reading request-scoped values:
class ServerOnly extends ElurComponent {
override onServerRender() {
// Runs only during renderToString / renderToChunks
}
override render() {
return html`<p>Hello</p>`;
}
}# Combining with effects
Effects created inside onMount are automatically cleaned up when you return
their dispose function:
class SearchBox extends ElurComponent {
private query = signal("");
override onMount() {
const stop = effect(() => {
console.log("Query:", this.query.value);
});
return stop; // disposed on unmount
}
override render() {
return html`<input @input=${(e: Event) => this.query.value = (e.target as HTMLInputElement).value} />`;
}
}Don't create effects in render(). render() runs once and sets up the
template — effects belong in onMount so their lifecycle is tied to the
component. Always return the effect's dispose function so it's cleaned up.
# Slots and children
Components receive child content through the children property or named slots.
Set them from the parent via setChildren() / setSlot():
class Card extends ElurComponent {
override render() {
return html`
<div class="card">
<div class="card-body">${this.children}</div>
</div>
`;
}
}
const card = new Card();
card.setChildren(html`<p>Hello from the parent</p>`);
mount(card, "#app");# ElurChildren type
The children property and setChildren() accept ElurChildren — a union
of ElurTemplate, ElurComponent, null, undefined, or an array of
those:
type ElurChildren =
| ElurTemplate
| ElurComponent
| null
| undefined
| ElurChildren[];This lets you pass a single child, multiple children, or nothing:
card.setChildren(html`<p>One child</p>`);
card.setChildren([html`<p>First</p>`, html`<p>Second</p>`]);
card.setChildren(null); // no children# Common patterns
# Fetch on mount
class UserProfile extends ElurComponent {
private user = signal<any>(null);
private loading = signal(true);
override onMount() {
fetch("/api/user/1")
.then((r) => r.json())
.then((data) => {
this.user.value = data;
this.loading.value = false;
});
}
override render() {
return html`
<div>
${() => this.loading.value
? html`<p>Loading…</p>`
: html`<p>${() => this.user.value.name}</p>`}
</div>
`;
}
}# Event listener cleanup
class Draggable extends ElurComponent {
private pos = signal({ x: 0, y: 0 });
override onMount() {
const handleMove = (e: MouseEvent) => {
this.pos.value = { x: e.clientX, y: e.clientY };
};
document.addEventListener("mousemove", handleMove);
return () => document.removeEventListener("mousemove", handleMove);
}
override render() {
return html`<div style=${() => `transform: translate(${this.pos.value.x}px, ${this.pos.value.y}px)`}>Drag me</div>`;
}
}Because Elur components run only once, there's no "re-mount" or "update"
lifecycle. Everything is set up in onInit/onMount and torn down in
onUnmount or via the cleanup function returned from onMount.