Skip to content

Lit Lifecycle

Every Lit component moves through a predictable sequence of stages, from construction through repeated updates to eventual disconnection. This lesson maps out the full lifecycle and every hook available at each stage.

The Three Phases of a Lit Component's Life

A Lit component's lifecycle breaks into three broad phases: a one-time setup phase (constructor, then connectedCallback when added to the DOM), a repeating update phase that runs every time reactive properties change (shouldUpdate, willUpdate, render, updated), and a teardown phase (disconnectedCallback) when the element is removed.

Most of these hooks come from the native Custom Elements spec (connectedCallback, disconnectedCallback) or from ReactiveElement's update cycle (shouldUpdate, willUpdate, updated, firstUpdated) — Lit doesn't invent a parallel lifecycle, it extends the platform's own hooks with reactive update scheduling.

class LifecycleDemo extends LitElement {
  constructor() { super(); console.log('1. constructor'); }
  connectedCallback() { super.connectedCallback(); console.log('2. connectedCallback'); }
  willUpdate(changed) { console.log('3. willUpdate', changed); }
  render() { console.log('4. render'); return html`<p>demo</p>`; }
  updated(changed) { console.log('5. updated', changed); }
  firstUpdated(changed) { console.log('6. firstUpdated', changed); }
  disconnectedCallback() { super.disconnectedCallback(); console.log('7. disconnectedCallback'); }
}

Logging every hook like this once, in a throwaway component, is the fastest way to build an intuitive feel for the exact order these methods run in.

Lifecycle Method Order

constructor()
  -> connectedCallback()
     -> willUpdate(changedProps)   [before every render]
        -> render()
           -> updated(changedProps) [after every render]
              -> firstUpdated(changedProps) [only after the very first render]
  -> disconnectedCallback()  [when removed from the DOM]
  • constructor() and connectedCallback() each run exactly once per element instance (per DOM connection).
  • willUpdate, render, and updated run together as a group on every single reactive update.
  • firstUpdated runs only once, immediately after the very first updated call.
  • disconnectedCallback runs when the element is removed from the document, and can run again if it's later re-attached and re-removed.

Lifecycle Hooks Cheat Sheet

Every hook, when it runs, and what it's typically used for.

Hook Runs Typical Use
constructor() Once, on instantiation Set default property values
connectedCallback() Each time added to the DOM Start subscriptions, fetch data
shouldUpdate(changed) Before every update, can cancel it Performance: skip unnecessary renders
willUpdate(changed) Before every render, sync style Compute derived state before rendering
render() Every update Return the template
updated(changed) After every render React to specific property changes, focus management
firstUpdated(changed) Once, after first render One-time DOM setup, measuring elements
disconnectedCallback() Each time removed from the DOM Clean up subscriptions, timers, observers

One-Time Hooks vs. Repeating Hooks

It's important to separate hooks that run once per connection (constructor, connectedCallback, firstUpdated) from hooks that run on every single reactive update (shouldUpdate, willUpdate, render, updated). Placing expensive one-time setup logic inside a repeating hook by mistake is a common source of performance bugs.

Hook Frequency
constructor() Once per instance
connectedCallback() Once per DOM connection (can repeat if moved/re-added)
firstUpdated() Exactly once, ever, per instance
willUpdate() / render() / updated() Every single reactive update

Reacting to a Specific Property Change

The changedProperties argument passed to willUpdate and updated is a Map of property names to their *previous* values, letting you react precisely to which property changed rather than re-running logic for every update regardless of cause.

updated(changedProperties) {
  if (changedProperties.has('selectedId')) {
    this.#scrollSelectedItemIntoView();
  }
}

Checking changedProperties.has(...) avoids running expensive logic (like scrolling or measuring) on unrelated updates.

Common Mistakes

  • Forgetting to call super.connectedCallback()/super.disconnectedCallback() when overriding them, breaking Lit's own internal setup/teardown.
  • Putting one-time setup logic inside render() or updated(), causing it to re-run on every single update.
  • Not cleaning up subscriptions, timers, or event listeners started in connectedCallback() inside the matching disconnectedCallback().
  • Reacting to every update inside updated() without checking changedProperties, causing unnecessary repeated work.

Key Takeaways

  • The lifecycle has three phases: one-time setup, a repeating update cycle, and teardown.
  • constructor, connectedCallback, and firstUpdated are one-time (per connection); willUpdate/render/updated repeat on every change.
  • Always call super.connectedCallback() and super.disconnectedCallback() when overriding them.
  • Use the changedProperties map to react precisely to which property changed, rather than to every update.

Pro Tip

Keep a mental (or literal, commented) checklist pairing every connectedCallback() side effect with its disconnectedCallback() cleanup — subscriptions, timers, and observers started without a matching teardown are the single most common source of memory leaks in real Lit components.