Advertisement
🦉 Lifecycle

OWL JS Component Lifecycle – Complete Overview

Every OWL component goes through a predictable sequence of phases from creation to destruction. Understanding this lifecycle lets you know exactly when to fetch data, when the DOM is ready to manipulate, when to set up and tear down subscriptions, and how to react to prop or state changes. OWL provides a set of lifecycle hooks — functions you register in setup() — that fire at precisely the right moment. This lesson maps out the entire lifecycle and gives you a clear mental model before the detailed hook lessons.

⏱️ 18 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 1 of 5

The Three Lifecycle Phases

An OWL component's life has three phases:

PhaseWhat happensRelevant hooks
Mounting Component is created, initial data loaded, template rendered, DOM inserted setup, onWillStart, onMounted
Updating State or props change, component re-renders, DOM patched onWillUpdateProps, onWillPatch, onPatched
Unmounting Component removed from the DOM, resources cleaned up onWillUnmount

There is also onError, which fires when a child component throws an error — it is not strictly a phase but a cross-cutting concern.

Lifecycle Flow Diagram

Lifecycle Flow
MOUNTING
  │
  ├─ setup()                  ← synchronous: call hooks, init state, get services
  │
  ├─ onWillStart()            ← async: fetch initial data before first render
  │
  ├─ [first render]           ← template compiled → virtual DOM produced
  │
  ├─ onMounted()              ← DOM is in the document: measure, focus, attach listeners
  │
UPDATING (on state change or new props)
  │
  ├─ onWillUpdateProps(nextProps)  ← async: prep before props update
  │      (only when props change)
  │
  ├─ onWillPatch()            ← sync: read current DOM state before patch
  │
  ├─ [re-render + DOM patch]  ← minimal DOM updates applied
  │
  ├─ onPatched()              ← sync: DOM fully updated, read new measurements
  │
UNMOUNTING
  │
  ├─ onWillUnmount()          ← sync: clear timers, listeners, subscriptions
  │
  └─ [component destroyed]

All Lifecycle Hooks — Reference

Hook Sync / Async Phase Fires when Main use case
setup() Sync Mount Component construction (before any render) Call all other hooks, get services, initialize state
onWillStart Async ✅ Mount Before first render (after setup) Fetch initial data, load resources before displaying
onMounted Sync Mount After first render and DOM insertion DOM manipulation, focus, scroll, attach global listeners
onWillUpdateProps Async ✅ Update Before re-render caused by new props from parent Fetch data based on new props before re-render
onWillPatch Sync Update Before every re-render (state or props change) Capture scroll position, read DOM before patch
onPatched Sync Update After every re-render (DOM patched) Restore scroll, measure new DOM, trigger side effects
onWillUnmount Sync Unmount Before component is removed from DOM Clear timers, remove event listeners, unsubscribe
onError Sync Any When a child component throws an uncaught error Error boundary: log error, show fallback UI

How to Register Lifecycle Hooks

All lifecycle hooks in OWL 2 are registered by calling the hook function inside setup(). They are imported from @odoo/owl.

JavaScript – registering hooks in setup()
import {
  Component, xml, useState,
  onWillStart, onMounted, onWillPatch,
  onPatched, onWillUnmount, onWillUpdateProps
} from "@odoo/owl";

class MyComponent extends Component {
  static template = xml`<div t-esc="state.data"/>`;

  // State can be declared as a class field (equivalent to calling useState in setup)
  state = useState({ data: null, isLoading: false });

  setup() {
    // All hook registrations go here
    onWillStart(async () => {
      this.state.isLoading = true;
      this.state.data = await fetchData();
      this.state.isLoading = false;
    });

    onMounted(() => {
      console.log("DOM is ready:", this.el);
    });

    onWillUnmount(() => {
      console.log("Cleanup time");
    });
  }
}
ℹ️
setup() is the OWL 2 way — not class methods

In OWL 1, lifecycle hooks were class methods you overrode (willStart(), mounted(), etc.). OWL 2 replaced these with composable hook functions called in setup(). This enables custom hooks — reusable hook compositions you can extract into separate functions and share across components. If you see old OWL 1 code using class method lifecycle hooks, those still work but are considered legacy.

Advertisement

setup() vs Class Field Declarations

Many things you would put in a constructor can be declared as class fields instead. Both are equivalent — use whichever reads more clearly:

JavaScript – class field vs setup
// ── Option A: class fields (concise, preferred for simple state) ──
class Counter extends Component {
  state = useState({ count: 0 });  // equivalent to calling useState in setup

  increment() { this.state.count++; }
}

// ── Option B: setup() (needed for hooks + service injection) ──
class Counter extends Component {
  setup() {
    this.state = useState({ count: 0 });

    // setup() is required when you also need lifecycle hooks
    onMounted(() => { console.log("mounted!"); });
  }

  increment() { this.state.count++; }
}

// ── Mixing both — common pattern ──
class UserDashboard extends Component {
  // Simple state as class field
  ui = useState({ isLoading: true, error: null });

  setup() {
    // Services must be obtained in setup()
    this.rpc     = useService("rpc");
    this.user    = useService("user");

    // Hooks must be called in setup()
    onWillStart(async () => {
      await this.loadData();
    });
  }
}

Child and Parent Lifecycle Order

When a parent component mounts and renders child components, the lifecycle hooks fire in a specific order — important to know when you depend on child DOM being available:

Text – lifecycle hook call order
Mounting order:
  1. Parent setup()
  2. Parent onWillStart()         ← awaited
  3. Parent first render begins
     4. Child setup()
     5. Child onWillStart()       ← awaited
     6. Child first render
     7. Child DOM inserted
     8. Child onMounted()
  9. Parent DOM fully inserted
  10. Parent onMounted()          ← parent fires AFTER all children mounted

Unmounting order:
  1. Parent onWillUnmount()       ← parent fires FIRST
  2. Children onWillUnmount()     ← then children, in reverse render order
  3. DOM removed
💡
onMounted: children are ready

When onMounted fires on the parent, all its children are already mounted. This means this.el in the parent's onMounted gives you the complete subtree, including child components' DOM output. This is useful when you need to measure the total rendered height of a section that includes child components.

Accessing the DOM: this.el

After a component is mounted, this.el points to the root DOM element rendered by the component's template. It is null before mounting and after unmounting.

JavaScript – this.el
class Panel extends Component {
  static template = xml`
    <div class="panel">
      <p>Panel content</p>
    </div>
  `;

  setup() {
    onMounted(() => {
      // this.el is the <div class="panel"> element
      const height = this.el.getBoundingClientRect().height;
      console.log("Panel height:", height);
    });

    onWillUnmount(() => {
      // this.el is still valid here — last chance to read/clean up DOM
      console.log("About to remove:", this.el);
    });
  }
}

Registering Multiple Hooks of the Same Type

You can call the same hook function more than once in setup() — each registration adds a callback to a list. This is what enables custom hooks to work: multiple pieces of code can independently register for the same lifecycle event.

JavaScript – multiple onMounted registrations
function useAutoFocus() {
  onMounted(() => {
    // This hook also registers onMounted
    const input = document.querySelector(".auto-focus-input");
    if (input) input.focus();
  });
}

class SearchPage extends Component {
  setup() {
    // Component registers its own onMounted
    onMounted(() => {
      console.log("Page mounted, logging analytics");
    });

    // Custom hook registers another onMounted internally
    useAutoFocus();

    // Both fire when the component mounts — order: registration order
  }
}

The OWL Lifecycle, Start to Finish

Every component moves through a fixed sequence of phases. Each has a hook where you run the right kind of code — data-loading before render, DOM work after mount, cleanup before removal.

OrderHookDOM?Do here
1setup()nostate, other hooks, services
2onWillStart()noasync: fetch initial data
3— render —OWL builds the DOM
4onMounted()yesrefs, focus, 3rd-party libs, listeners
5onWillUpdateProps()yesreact to new props
6onPatched()yesafter a re-render
7onWillUnmount()yescleanup — timers, listeners
setup() {
  onWillStart(async () => { this.data = await load(); });  // before first paint
  onMounted(() => { this.chart = new Chart(this.ref.el); }); // DOM ready
  onWillUnmount(() => { this.chart.destroy(); });            // no leaks
}

The golden pairing: anything you create in onMounted (a chart, an interval, a global event listener) must be torn down in onWillUnmount. Skip it and every mount leaks a little more memory. All hooks must be registered synchronously inside setup().

🏋️ Practical Exercise

Build a LifecycleLogger component to visualize the lifecycle in action:

  1. State: an array log of strings (event names + timestamps).
  2. Register all 6 hooks in setup(). In each, push a timestamped string to this.log (a plain array, not state — you'll update state from outside).
  3. Wait — actually use useState for the log and push entries from each hook. Watch the log grow as you interact.
  4. Add a "Trigger Update" button that mutates some state to fire the update-phase hooks.
  5. Wrap LifecycleLogger in a parent that has a "Unmount" button (uses t-if) to trigger the unmount phase.
  6. Observe the order: which hooks fire first on mount? On update? On unmount?

🔥 Challenge Exercise

Map OWL’s component lifecycle: setup, onWillStart, onMounted, onWillUpdateProps, onWillPatch/onPatched, onWillUnmount. Add logging to each and observe the sequence on mount, update, and unmount. Explain what work belongs in each phase.

📋 Summary

  • OWL lifecycle has three phases: Mounting (setup → onWillStart → render → onMounted), Updating (onWillUpdateProps → onWillPatch → render → onPatched), Unmounting (onWillUnmount).
  • All hooks are registered by calling hook functions inside setup(). They are imported from @odoo/owl.
  • onWillStart and onWillUpdateProps are async — OWL awaits them before rendering.
  • All other hooks are synchronous — long operations in them block the render pipeline.
  • Children mount before parents; parents unmount before children.
  • this.el is the component's root DOM element — available from onMounted until after onWillUnmount.
  • Multiple registrations of the same hook are allowed — they all fire in registration order.

Interview Questions

  • What are OWL’s lifecycle hooks?
  • In what order do they run?
  • What is the difference between setup and onMounted?
  • Which hook fires before removal?
  • Where do you fetch initial data?

FAQ

Can I use async in onMounted or onWillUnmount? +

Technically yes, but OWL does not await them. onMounted and onWillUnmount are synchronous from OWL's perspective — if you pass an async function, OWL calls it and ignores the returned Promise. Any async work continues in the background without blocking the render. For pre-render async work, use onWillStart (which OWL does await). For post-mount async side effects, use onMounted and handle the Promise yourself, but be careful about setting state after the component might have unmounted.

What is the difference between onWillPatch and onWillUpdateProps? +

onWillUpdateProps fires only when the parent passes new props — it gives you the incoming next props before re-render and can be async. onWillPatch fires before every re-render, whether caused by state change, props change, or a forced re-render — it is always synchronous. Use onWillUpdateProps to react to prop changes (e.g., fetch new data for a new ID prop). Use onWillPatch to snapshot DOM state before any re-render.

Does onMounted fire when the component re-renders? +

No — onMounted fires exactly once per component instance, after the first render inserts the DOM. Subsequent re-renders fire onWillPatch and onPatched instead. If you need to run code after every DOM update (not just the first), use onPatched. If you need to run code only after the first render and also after updates, register both onMounted and onPatched.

What happens if I throw in onWillStart? +

If an onWillStart callback throws (or its Promise rejects), OWL catches the error and propagates it up to the nearest ancestor component that has registered an onError hook. If no ancestor handles it, OWL throws it as an unhandled error. The component that threw never renders. This makes onError the right place to implement error boundaries — show fallback UI when a child fails to initialize.