Skip to content

Controller Lifecycle

A reactive controller's lifecycle hooks mirror the host component's own lifecycle. This lesson covers each hook in depth, with concrete timing relative to the host's callbacks.

How Controller Hooks Map to Host Hooks

Every ReactiveController lifecycle method is called by the host component at a specific, well-defined point in the host's own lifecycle — hostConnected() runs during the host's connectedCallback, hostDisconnected() during disconnectedCallback, and hostUpdate()/hostUpdated() around the host's own willUpdate/updated pair.

If a host component has multiple controllers, each one's matching hook is called in the order the controllers were added, before the host's own corresponding method runs (for hostUpdate) or after (for hostUpdated) — this ordering matters if controllers need to coordinate.

class LoggingController {
  constructor(host) { host.addController(this); }
  hostConnected() { console.log('controller: connected'); }
  hostUpdate() { console.log('controller: about to update'); }
  hostUpdated() { console.log('controller: finished updating'); }
  hostDisconnected() { console.log('controller: disconnected'); }
}

class DemoElement extends LitElement {
  #logger = new LoggingController(this);
  connectedCallback() { super.connectedCallback(); console.log('host: connected'); }
  updated() { console.log('host: updated'); }
}
// Console order: "controller: connected", "host: connected",
//                "controller: about to update", "host: updated"... (render happens between)
//                "controller: finished updating"

Controller hooks generally run around the host's matching lifecycle point — before for connect/update-start, after for update-finish.

The Full ReactiveController Interface

interface ReactiveController {
  hostConnected?(): void;
  hostDisconnected?(): void;
  hostUpdate?(): void;
  hostUpdated?(): void;
}
  • Every method is optional — implement only the hooks your controller actually needs.
  • None of these methods receive changedProperties directly — a controller typically tracks whatever state it cares about internally.
  • A controller can read and write directly to this.host (a reference to the component instance) if it needs to inspect the host's own properties.
  • host.requestUpdate() remains the mechanism for triggering a re-render — none of these lifecycle hooks trigger one automatically by themselves.

Controller Hook Timing Reference

Exactly when each hook fires relative to the host's own lifecycle.

Controller Hook Fires Relative To
hostConnected() During the host's connectedCallback
hostUpdate() Just before the host's willUpdate/render
hostUpdated() Just after the host's updated
hostDisconnected() During the host's disconnectedCallback

Multiple Controllers on One Host

Because controllers are just registered instances in a list the host maintains internally, a component can host as many controllers as it needs, each handling one focused concern — for example, one controller for media query tracking, another for keyboard shortcut handling, and another for a shared data store subscription, all on the same component.

class Dashboard extends LitElement {
  #mediaQuery = new MediaQueryController(this, '(max-width: 600px)');
  #shortcuts = new KeyboardShortcutsController(this, { 'Escape': () => this.close() });
  #store = new StoreController(this, appStore);
}

Calling requestUpdate() from Within a Hook

It's perfectly valid — and common — for a controller to call this.host.requestUpdate() from inside its own event handlers (as shown in the previous lesson) rather than from within one of these four lifecycle hooks directly. The hooks are about connecting to the host's lifecycle timing; the actual trigger for a re-render is usually an external event (a media query change, a store update) the controller is listening for.

Common Mistakes

  • Expecting a controller lifecycle hook to receive changedProperties the way updated() does on the host itself — it does not.
  • Assuming controller hooks automatically trigger a host re-render — you still need an explicit host.requestUpdate() call.
  • Registering the same controller instance with addController more than once by accident, causing its hooks to run multiple times per host lifecycle event.
  • Forgetting that hostUpdate()/hostUpdated() run on every single host update, not just the first one — treat them like willUpdate/updated, not like firstUpdated.

Key Takeaways

  • Controller lifecycle hooks (hostConnected, hostDisconnected, hostUpdate, hostUpdated) mirror the host's own lifecycle timing.
  • All four hooks are optional — implement only what a given controller actually needs.
  • A host component can register multiple controllers, each handling one focused, independent concern.
  • Controllers still need an explicit host.requestUpdate() call to trigger a re-render — lifecycle hooks alone don't do that automatically.

Pro Tip

When debugging multi-controller interaction issues, temporarily add a console.log to each hook (as in the overview example) — seeing the actual interleaved order for your specific host and controllers is far more reliable than reasoning about timing from documentation alone.