- The six lifecycle hooks and their execution order
- When to use
initialState()vsconnected() - How
updated(changedKeys)helps you react to state mutations - Cleanup patterns with
disconnected()andonCleanup() - The difference between
connected()andreconnected()
The lifecycle diagram
When the browser encounters a custom element, it goes through a fixed sequence:
constructor() → called once by the browser (never override unless you know why)
prepare(fragment) → inspect Light DOM children before first render
initialState() → return the initial reactive state object
template() → render the component HTML (called on mount and every state change)
connected() → first attachment complete — setup listeners, timers, stores
reconnected() → called on every re-attachment after initialization
disconnected() → leaving the DOM — cleanup time
Hook 1: initialState()
Returns a plain object that becomes this.state. Safe to read data-* attributes here.
class LifecycleTimer extends BaseComponent {
initialState() {
return {
elapsed: 0,
running: this.bool('running', true)
};
}
}
Rule
Return a flat object from initialState(). Use this.str(), this.num(), this.bool(), or this.json() to seed values from HTML attributes. Deep nesting works but keep it shallow for clarity.
Hook 2: template()
Must return an HtmlString from the html tagged template. This is called on mount and every time state changes.
template() {
return html`
<div>
<output>${this.state.elapsed}s elapsed</output>
<button data-on:click="toggle">
${this.state.running ? 'Pause' : 'Resume'}
</button>
</div>`;
}
Hook 3: connected()
Called once after the first render. Perfect for setting up timers, event listeners, and store subscriptions.
connected() {
this._interval = setInterval(() => {
if (this.state.running) {
this.state.elapsed += 1;
}
}, 1000);
}
Hook 4: reconnected()
Called every time the element is moved back into the DOM after the first connection. Use it to restart paused work.
reconnected() {
// Element was removed and re-attached. Resume any paused work.
console.log('Re-attached, elapsed:', this.state.elapsed);
}
Hook 5: disconnected()
Called when the element leaves the DOM. Clean up timers, remove listeners, unsubscribe from stores.
disconnected() {
clearInterval(this._interval);
}
For convenience, use this.listen() and this.onCleanup() instead of manual cleanup:
connected() {
this.listen(window, 'resize', this.handleResize);
this.onCleanup(() => console.log('Component removed'));
}
Tip
this.listen(target, event, handler) adds a listener that is automatically removed when the component disconnects. this.onCleanup(fn) registers a cleanup function that runs on disconnect.
Hook 6: updated(changedKeys)
Called after any state-driven re-render except the first one. Receives an array of dot-path keys that changed.
updated(changedKeys) {
if (changedKeys.includes('running')) {
console.log('Timer was', this.state.running ? 'resumed' : 'paused');
}
if (changedKeys.includes('elapsed')) {
this.emit('tick', { elapsed: this.state.elapsed });
}
}
Full example: Auto-counter
This counter increments automatically. It emits dv:tick every second.
import { BaseComponent, html, define } from '/path/to/core.js';
class DvAutoCounter extends BaseComponent {
initialState() {
return { count: 0, running: this.bool('running', true) };
}
template() {
return html`
<output>${this.state.count}</output>
<button data-on:click="toggle">
${this.state.running ? '⏸' : '▶'}
</button>`;
}
connected() {
this._timer = setInterval(() => {
if (this.state.running) this.state.count += this.num('speed', 1);
}, 1000);
}
toggle() { this.state.running = !this.state.running; }
disconnected() { clearInterval(this._timer); }
updated(changedKeys) {
if (changedKeys.includes('count')) this.emit('tick', { count: this.state.count });
}
}
define('dv-auto-counter', DvAutoCounter);
Quick reference
| Hook | When | Use for |
|---|---|---|
prepare(fragment) | Before first render | Inspecting Light DOM children |
initialState() | Before first render | Returning initial reactive state |
template() | Mount + every state change | Rendering HTML |
connected() | After first render | Timers, listeners, stores |
reconnected() | Every re-attachment | Resuming paused work |
disconnected() | When removed from DOM | Cleanup, unsubscribe |
updated(keys) | After state-driven re-render | Side effects, analytics, emits |