Skip to content
DevinimJS
Phase 1 · Lesson 2

Component Lifecycle — The Six Hooks

Every DevinimJS component follows a predictable lifecycle. Learn the six hooks that give you control at each stage.

What you'll learn
  • The six lifecycle hooks and their execution order
  • When to use initialState() vs connected()
  • How updated(changedKeys) helps you react to state mutations
  • Cleanup patterns with disconnected() and onCleanup()
  • The difference between connected() and reconnected()

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

Live demo — auto-incrementing timer

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

HookWhenUse for
prepare(fragment)Before first renderInspecting Light DOM children
initialState()Before first renderReturning initial reactive state
template()Mount + every state changeRendering HTML
connected()After first renderTimers, listeners, stores
reconnected()Every re-attachmentResuming paused work
disconnected()When removed from DOMCleanup, unsubscribe
updated(keys)After state-driven re-renderSide effects, analytics, emits