The Three Lifecycle Phases
An OWL component's life has three phases:
| Phase | What happens | Relevant 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
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.
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");
});
}
}
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.
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:
// ── 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:
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
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.
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.
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.
| Order | Hook | DOM? | Do here |
|---|---|---|---|
| 1 | setup() | no | state, other hooks, services |
| 2 | onWillStart() | no | async: fetch initial data |
| 3 | — render — | — | OWL builds the DOM |
| 4 | onMounted() | yes | refs, focus, 3rd-party libs, listeners |
| 5 | onWillUpdateProps() | yes | react to new props |
| 6 | onPatched() | yes | after a re-render |
| 7 | onWillUnmount() | yes | cleanup — 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:
- State: an array
logof strings (event names + timestamps). - Register all 6 hooks in
setup(). In each, push a timestamped string tothis.log(a plain array, not state — you'll update state from outside). - Wait — actually use
useStatefor the log and push entries from each hook. Watch the log grow as you interact. - Add a "Trigger Update" button that mutates some state to fire the update-phase hooks.
- Wrap
LifecycleLoggerin a parent that has a "Unmount" button (usest-if) to trigger the unmount phase. - 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. onWillStartandonWillUpdatePropsare 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.elis the component's root DOM element — available fromonMounteduntil afteronWillUnmount.- 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
setupandonMounted? - Which hook fires before removal?
- Where do you fetch initial data?
Related Topics
FAQ
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.
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.
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.
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.

