Why this chapter? More production incidents I've dealt with trace back to lifecycle mistakes than to almost anything else — a fetch that fires before the DOM exists, a listener that never gets cleaned up and slowly leaks memory across a long-running session. What is Odoo trying to solve with this? A backend view can mount and unmount dozens of widgets as a user navigates — lifecycle hooks are how Odoo guarantees each one initializes and tears down cleanly, without leftover timers or listeners piling up. Real-world application: Loading a partner's related records before first render (
onWillStart), focusing an input when a form opens (onMounted), and clearing an interval when a kanban card is removed (onWillUnmount) are all lifecycle hooks doing exactly the job this chapter describes.
A component, much like a living organism, has a lifecycle. It is born (created and mounted to the DOM), it lives (updates in response to state and prop changes), and eventually, it dies (is unmounted and removed from the DOM).
OWL provides a powerful way to tap into these key moments in a component's life using lifecycle hooks. These hooks are special functions that you can call within your setup method to register callbacks that will execute at specific points in the lifecycle.
Understanding these hooks is essential for managing side effects, fetching data correctly, and cleaning up resources to prevent memory leaks. Let's explore the most important hooks you'll use in your day-to-day development.
The Lifecycle at a Glance
Before diving into individual hooks, let's visualize the complete journey of a component:
- Creation Phase:
setup(): The component's main setup logic runs and registers the hooks.onWillStart: Runs before the first render, perfect for initial data fetching.-
Component instance created and prepared.
-
Mounting Phase (Added to the page):
- The component renders its HTML template.
- The HTML is inserted into the DOM.
-
onMounted: The component is now live on the page. -
Update Phase (During its life - repeats as needed):
- Parent passes new props →
onWillUpdateProps. - State changes trigger re-render.
- The component re-renders its HTML.
-
DOM is updated with changes.
-
Unmounting Phase (Removal from the page):
onWillUnmount: The component is about to be destroyed.- The component is removed from the DOM.
- Memory and resources are cleaned up.
{width=100%}
onWillStart: Fetching Initial Data
The onWillStart hook is unique because it's the only hook that can be asynchronous. It runs before the component's initial render, making it the perfect place to fetch data that the component needs for its very first appearance.
- When it runs: After
setup()finishes, but before the initial render. - Key feature: Can be async - the component waits for the promise to resolve.
- Use case: Fetching essential data that must be ready before first render.
Basic Example: Product List
Let's create a ProductList component that fetches products before rendering:
JavaScript (product_list.js):
import { Component, onWillStart, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class ProductList extends Component {
static template = "my_module.ProductList";
setup() {
this.state = useState({
products: [],
loading: true,
error: null
});
this.orm = useService("orm");
onWillStart(async () => {
try {
console.log("ProductList: Fetching products before first render...");
const data = await this.orm.searchRead(
"product.product",
[],
["name", "list_price", "categ_id"]
);
this.state.products = data;
console.log(`ProductList: Loaded ${data.length} products`);
} catch (error) {
console.error("Failed to load products:", error);
this.state.error = "Failed to load products";
} finally {
this.state.loading = false;
}
});
}
get formattedProducts() {
return this.state.products.map(product => ({
...product,
formattedPrice: `$${product.list_price.toFixed(2)}`
}));
}
}
Template (product_list.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.ProductList" owl="1">
<div class="product-list">
<!-- Loading State -->
<t t-if="state.loading">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">Loading products...</p>
</div>
</t>
<!-- Error State -->
<t t-elif="state.error">
<div class="alert alert-danger" t-esc="state.error"/>
</t>
<!-- Products Grid -->
<t t-else="">
<div class="row">
<t t-foreach="formattedProducts" t-as="product" t-key="product.id">
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title" t-esc="product.name"/>
<p class="card-text">
<strong t-esc="product.formattedPrice"/>
</p>
<small class="text-muted" t-esc="product.categ_id[1]"/>
</div>
</div>
</div>
</t>
</div>
</t>
</div>
</t>
</templates>
By fetching data in onWillStart, you prevent the component from rendering an empty state and then "flickering" when the data arrives. The component will wait for the onWillStart promise to resolve before performing its initial render.
Advanced onWillStart Patterns
Multiple Async Operations:
onWillStart(async () => {
// Run multiple operations in parallel
const [products, categories, suppliers] = await Promise.all([
this.orm.searchRead("product.product", [], ["name", "list_price"]),
this.orm.searchRead("product.category", [], ["name"]),
this.orm.searchRead("res.partner", [["supplier_rank", ">", 0]], ["name"])
]);
this.state.products = products;
this.state.categories = categories;
this.state.suppliers = suppliers;
});
Error Recovery Pattern:
onWillStart(async () => {
try {
// Try primary data source
this.state.data = await this.fetchFromPrimarySource();
} catch (primaryError) {
console.warn("Primary source failed, trying fallback:", primaryError);
try {
// Try fallback data source
this.state.data = await this.fetchFromFallbackSource();
this.state.usingFallback = true;
} catch (fallbackError) {
console.error("All data sources failed:", fallbackError);
this.state.error = "Unable to load data from any source";
}
}
});
onMounted: Interacting with the DOM
The onMounted hook runs after the component has been rendered for the first time and its HTML has been added to the DOM. This is your gateway to DOM manipulation and third-party library integration.
- When it runs: After the initial render and DOM insertion.
- Use case: Any task that requires the component's HTML to exist in the DOM.
Key Use Cases:
- Focus input elements
- Initialize third-party libraries (charts, maps, calendars)
- Measure element dimensions
- Set up DOM event listeners
To get a reference to a specific element in your template, you use the t-ref directive and the useRef hook.
Example: Auto-Focus Search Component
JavaScript (search_component.js):
import { Component, onMounted, useRef, useState } from "@odoo/owl";
export class SearchComponent extends Component {
static template = "my_module.SearchComponent";
setup() {
this.state = useState({
searchTerm: "",
results: []
});
// Create a reference to the search input
this.searchInputRef = useRef("search-input");
onMounted(() => {
console.log("SearchComponent: Component mounted, focusing input");
// The input element is now in the DOM and can be focused
if (this.searchInputRef.el) {
this.searchInputRef.el.focus();
console.log("SearchComponent: Input focused successfully");
}
});
}
onSearchInput(event) {
this.state.searchTerm = event.target.value;
console.log("Search term changed:", this.state.searchTerm);
// Here you could trigger a search API call
}
}
Template (search_component.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.SearchComponent" owl="1">
<div class="search-component">
<div class="search-container mb-3">
<input
type="text"
class="form-control"
t-ref="search-input"
placeholder="Search products..."
t-model="state.searchTerm"
t-on-input="onSearchInput"
/>
</div>
<div class="search-results">
<t t-if="state.searchTerm">
<p class="text-muted">
Searching for: "<t t-esc="state.searchTerm"/>"
</p>
</t>
<!-- Results would go here -->
<div class="results-list">
<!-- Results implementation -->
</div>
</div>
</div>
</t>
</templates>
Advanced onMounted Example: Chart Integration
Here's a more complex example that integrates with Chart.js:
JavaScript (sales_chart.js):
import { Component, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class SalesChart extends Component {
static template = "my_module.SalesChart";
setup() {
this.state = useState({
chartData: [],
loading: true
});
this.chartCanvasRef = useRef("sales-chart");
this.orm = useService("orm");
this.chartInstance = null;
onMounted(async () => {
console.log("SalesChart: Component mounted, initializing chart");
await this.loadSalesData();
this.initializeChart();
});
onWillUnmount(() => {
// Clean up the chart when component is destroyed
if (this.chartInstance) {
console.log("SalesChart: Destroying chart instance");
this.chartInstance.destroy();
}
});
}
async loadSalesData() {
try {
const salesData = await this.orm.searchRead(
"sale.order",
[["state", "=", "sale"]],
["date_order", "amount_total"]
);
this.state.chartData = salesData;
this.state.loading = false;
} catch (error) {
console.error("Failed to load sales data:", error);
this.state.loading = false;
}
}
initializeChart() {
if (!this.chartCanvasRef.el || this.state.loading) {
return;
}
// Assuming Chart.js is loaded globally
const ctx = this.chartCanvasRef.el.getContext('2d');
// Process data for chart
const processedData = this.processChartData();
this.chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: processedData.labels,
datasets: [{
label: 'Sales Revenue',
data: processedData.data,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Sales Performance'
}
}
}
});
console.log("SalesChart: Chart initialized successfully");
}
processChartData() {
// Group sales by month and sum amounts
const monthlyData = {};
this.state.chartData.forEach(order => {
const month = order.date_order.substring(0, 7); // YYYY-MM format
if (!monthlyData[month]) {
monthlyData[month] = 0;
}
monthlyData[month] += order.amount_total;
});
const sortedMonths = Object.keys(monthlyData).sort();
return {
labels: sortedMonths,
data: sortedMonths.map(month => monthlyData[month])
};
}
}
Template (sales_chart.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.SalesChart" owl="1">
<div class="sales-chart">
<t t-if="state.loading">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading chart data...</span>
</div>
</div>
</t>
<t t-else="">
<div class="chart-container">
<canvas t-ref="sales-chart" width="400" height="200"></canvas>
</div>
</t>
</div>
</t>
</templates>
onWillUpdateProps: Reacting to Prop Changes
This hook is called when a parent component is about to pass new props to the child. It allows the child to react to the incoming changes before it re-renders itself.
- When it runs: When new props are received, before the component updates.
- Use case: When a child component needs to fetch new data based on changed props.
Example: User Profile Component
Imagine a UserProfile component that displays user details. When the parent changes the userId prop, the component needs to fetch the new user's data:
JavaScript (user_profile.js):
import { Component, onWillStart, onWillUpdateProps, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class UserProfile extends Component {
static template = "my_module.UserProfile";
static props = {
userId: { type: Number }
};
setup() {
this.state = useState({
user: null,
loading: true,
error: null
});
this.orm = useService("orm");
// Load initial user data
onWillStart(async () => {
await this.loadUser(this.props.userId);
});
// React to prop changes
onWillUpdateProps(async (nextProps) => {
console.log("UserProfile: Props will update", {
current: this.props.userId,
next: nextProps.userId
});
// Only reload if the user ID actually changed
if (this.props.userId !== nextProps.userId) {
console.log(`UserProfile: User ID changed from ${this.props.userId} to ${nextProps.userId}`);
this.state.loading = true;
await this.loadUser(nextProps.userId);
}
});
}
async loadUser(userId) {
try {
console.log(`UserProfile: Loading user data for ID ${userId}`);
const users = await this.orm.read("res.users", [userId], [
"name",
"email",
"phone",
"partner_id"
]);
if (users.length > 0) {
this.state.user = users[0];
this.state.error = null;
console.log(`UserProfile: Successfully loaded user ${users[0].name}`);
} else {
throw new Error(`User with ID ${userId} not found`);
}
} catch (error) {
console.error(`Failed to load user ${userId}:`, error);
this.state.error = `Failed to load user: ${error.message}`;
this.state.user = null;
} finally {
this.state.loading = false;
}
}
}
Template (user_profile.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.UserProfile" owl="1">
<div class="user-profile card">
<div class="card-header">
<h5>User Profile</h5>
</div>
<div class="card-body">
<!-- Loading State -->
<t t-if="state.loading">
<div class="text-center py-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading user...</span>
</div>
<p class="mt-2 text-muted">Loading user profile...</p>
</div>
</t>
<!-- Error State -->
<t t-elif="state.error">
<div class="alert alert-danger" t-esc="state.error"/>
</t>
<!-- User Data -->
<t t-elif="state.user">
<div class="user-info">
<h6 class="mb-3" t-esc="state.user.name"/>
<div class="user-details">
<div class="mb-2">
<strong>Email:</strong>
<span t-esc="state.user.email"/>
</div>
<t t-if="state.user.phone">
<div class="mb-2">
<strong>Phone:</strong>
<span t-esc="state.user.phone"/>
</div>
</t>
<div class="mb-2">
<strong>Partner ID:</strong>
<span t-esc="state.user.partner_id[1]"/>
</div>
</div>
</div>
</t>
</div>
</div>
</t>
</templates>
Parent Component Example
Here's how a parent component might use the UserProfile:
JavaScript (user_manager.js):
import { Component, useState } from "@odoo/owl";
import { UserProfile } from "../user_profile/user_profile";
export class UserManager extends Component {
static template = "my_module.UserManager";
static components = { UserProfile };
setup() {
this.state = useState({
selectedUserId: 1,
availableUsers: [
{ id: 1, name: "Alice Johnson" },
{ id: 2, name: "Bob Smith" },
{ id: 3, name: "Carol Brown" }
]
});
}
selectUser(userId) {
console.log("UserManager: Selecting user", userId);
this.state.selectedUserId = userId;
}
}
Template (user_manager.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.UserManager" owl="1">
<div class="user-manager">
<div class="row">
<!-- User Selection -->
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h6>Select User</h6>
</div>
<div class="card-body">
<t t-foreach="state.availableUsers" t-as="user" t-key="user.id">
<button
class="btn btn-outline-primary btn-sm me-2 mb-2"
t-att-class="state.selectedUserId === user.id ? 'btn btn-primary btn-sm me-2 mb-2' : 'btn btn-outline-primary btn-sm me-2 mb-2'"
t-on-click="() => this.selectUser(user.id)">
<t t-esc="user.name"/>
</button>
</t>
</div>
</div>
</div>
<!-- User Profile Display -->
<div class="col-md-8">
<UserProfile userId="state.selectedUserId"/>
</div>
</div>
</div>
</t>
</templates>
This pattern ensures that the UserProfile component always shows data consistent with the userId prop it receives from its parent. When the user clicks a different user button in the parent, the UserProfile will automatically detect the prop change and fetch the new user's data.
onWillUnmount: Cleaning Up
The onWillUnmount hook is the component's last breath. It runs just before the component is removed from the DOM. This is crucial for cleanup to prevent memory leaks.
- When it runs: Right before the component is destroyed.
- Critical use case: Clean up resources that would otherwise persist after component destruction.
What to Clean Up:
- Timers and intervals (
setInterval,setTimeout) - Global event listeners (on
window,document) - Third-party library instances (charts, maps, etc.)
- WebSocket connections
- Ongoing API requests (if cancellable)
Example: Timer-Based Component
JavaScript (live_clock.js):
import { Component, onMounted, onWillUnmount, useState } from "@odoo/owl";
export class LiveClock extends Component {
static template = "my_module.LiveClock";
setup() {
this.state = useState({
currentTime: new Date().toLocaleTimeString(),
isRunning: true
});
// Store timer reference for cleanup
this.timer = null;
onMounted(() => {
console.log("LiveClock: Starting clock timer");
this.startClock();
});
onWillUnmount(() => {
console.log("LiveClock: Cleaning up clock timer");
this.stopClock();
});
}
startClock() {
// Update time every second
this.timer = setInterval(() => {
if (this.state.isRunning) {
this.state.currentTime = new Date().toLocaleTimeString();
}
}, 1000);
}
stopClock() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
console.log("LiveClock: Timer cleared successfully");
}
}
toggleClock() {
this.state.isRunning = !this.state.isRunning;
console.log("LiveClock: Clock", this.state.isRunning ? "resumed" : "paused");
}
}
Template (live_clock.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.LiveClock" owl="1">
<div class="live-clock card">
<div class="card-body text-center">
<h3 class="display-4 mb-3" t-esc="state.currentTime"/>
<button
class="btn btn-primary"
t-on-click="toggleClock">
<t t-esc="state.isRunning ? 'Pause' : 'Resume'"/>
</button>
</div>
</div>
</t>
</templates>
Advanced Cleanup Example: Multiple Resources
JavaScript (notification_system.js):
import { Component, onMounted, onWillUnmount, useState } from "@odoo/owl";
export class NotificationSystem extends Component {
static template = "my_module.NotificationSystem";
setup() {
this.state = useState({
notifications: [],
isConnected: false
});
// Store cleanup references
this.cleanupTasks = [];
onMounted(() => {
this.initializeSystem();
});
onWillUnmount(() => {
console.log("NotificationSystem: Performing comprehensive cleanup");
this.performCleanup();
});
}
initializeSystem() {
// 1. Set up periodic health check
const healthCheckTimer = setInterval(() => {
this.checkSystemHealth();
}, 5000);
this.cleanupTasks.push(() => {
clearInterval(healthCheckTimer);
console.log("NotificationSystem: Health check timer cleared");
});
// 2. Set up window focus/blur listeners
const handleWindowFocus = () => {
console.log("NotificationSystem: Window focused, refreshing notifications");
this.refreshNotifications();
};
const handleWindowBlur = () => {
console.log("NotificationSystem: Window blurred");
};
window.addEventListener('focus', handleWindowFocus);
window.addEventListener('blur', handleWindowBlur);
this.cleanupTasks.push(() => {
window.removeEventListener('focus', handleWindowFocus);
window.removeEventListener('blur', handleWindowBlur);
console.log("NotificationSystem: Window event listeners removed");
});
// 3. Set up online/offline listeners
const handleOnline = () => {
this.state.isConnected = true;
this.refreshNotifications();
};
const handleOffline = () => {
this.state.isConnected = false;
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
this.cleanupTasks.push(() => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
console.log("NotificationSystem: Network event listeners removed");
});
// 4. Initialize connection state
this.state.isConnected = navigator.onLine;
}
Notice the pattern: every time initializeSystem sets something up (a timer, a pair of listeners), it immediately pushes a matching "undo" function onto this.cleanupTasks. That's what makes the single call to performCleanup() below able to reverse everything, no matter how many resources were started.
checkSystemHealth() {
// Simulate system health check
console.log("NotificationSystem: Performing health check");
}
refreshNotifications() {
// Simulate notification refresh
console.log("NotificationSystem: Refreshing notifications");
}
performCleanup() {
// Execute all cleanup tasks
this.cleanupTasks.forEach((cleanup, index) => {
try {
cleanup();
} catch (error) {
console.error(`NotificationSystem: Cleanup task ${index} failed:`, error);
}
});
// Clear the cleanup tasks array
this.cleanupTasks = [];
console.log("NotificationSystem: All cleanup tasks completed");
}
}
If you forget to cleanup in onWillUnmount, resources like timers would keep running in the background forever, even after the component is gone, creating memory leaks. Using onWillUnmount for cleanup is a critical part of writing robust applications.
Lifecycle Best Practices
DO's
- Always cleanup in onWillUnmount - Clear timers, remove event listeners, destroy third-party instances
- Use onWillStart for critical data - Data that must be ready before first render
- Make onWillStart async - Take advantage of its unique async capability
- Handle errors gracefully - Wrap async operations in try-catch blocks
- Log lifecycle events - Helps with debugging during development
DON'Ts
- Never mutate props - Props are read-only in all lifecycle hooks
- Don't forget cleanup - Memory leaks will hurt your application's performance
- Don't assume DOM exists - Only
onMountedand later hooks have DOM access - Don't perform heavy computations - Keep lifecycle hooks fast and focused
Common Patterns
Pattern 1: Conditional Data Loading
onWillUpdateProps(async (nextProps) => {
// Only fetch if the important prop changed
if (this.props.customerId !== nextProps.customerId) {
await this.loadCustomerData(nextProps.customerId);
}
});
Pattern 2: Cleanup Helper
setup() {
this.cleanupFunctions = [];
onMounted(() => {
// Set up something that needs cleanup
const timer = setInterval(this.updateData, 1000);
this.cleanupFunctions.push(() => clearInterval(timer));
});
onWillUnmount(() => {
this.cleanupFunctions.forEach(cleanup => cleanup());
});
}
Pattern 3: Error Boundary
onWillStart(async () => {
try {
await this.loadCriticalData();
} catch (error) {
console.error("Failed to load critical data:", error);
this.state.error = "System temporarily unavailable";
// Could also dispatch to parent or show fallback UI
}
});
Debugging Lifecycle Issues
When working with lifecycle hooks, common issues include:
Issue: Component doesn't update when props change
Solution: Add onWillUpdateProps handler:
onWillUpdateProps(async (nextProps) => {
if (this.props.dataId !== nextProps.dataId) {
await this.loadNewData(nextProps.dataId);
}
});
Issue: Memory leaks from forgotten timers
Solution: Always clear in onWillUnmount:
onMounted(() => {
this.timer = setInterval(this.doSomething, 1000);
});
onWillUnmount(() => {
if (this.timer) {
clearInterval(this.timer);
}
});
Issue: Component renders empty then flickers when data loads
Solution: Use onWillStart instead of onMounted:
// This causes flicker
onMounted(async () => {
this.state.data = await this.fetchData();
});
// This prevents flicker
onWillStart(async () => {
this.state.data = await this.fetchData();
});
Error Handling with onError and Error Boundaries
Everything so far assumes rendering goes smoothly. It doesn't always. If a child component throws while rendering—a null reference, a bad API response shape, anything—OWL's default behavior is to destroy the entire application, not just the component that failed. One bug in a small widget can take down the whole page.
An error boundary is a component that catches errors thrown anywhere in its subtree and shows a fallback UI instead of letting the crash propagate. OWL provides the onError hook for exactly this.
onError(callback) registers a function that OWL calls whenever a rendering or lifecycle error occurs in one of this component's descendants. It does not catch errors thrown inside event handlers (a t-on-click handler that throws is your own try/catch territory)—only errors from the render/lifecycle machinery.
import { Component, useState, onError, xml } from "@odoo/owl";
class ErrorBoundary extends Component {
static template = xml`
<t t-if="state.error" t-slot="fallback">
<div class="alert alert-danger">Something went wrong.</div>
</t>
<t t-else="" t-slot="default"/>`;
setup() {
this.state = useState({ error: false });
onError(() => {
this.state.error = true;
});
}
}
Wrap any part of the tree you don't fully trust (a widget rendering user-generated content, a third-party integration) with this component, passing the risky content as its default slot and an optional fallback slot for a custom message. If your own onError handler can't recover, it can re-throw the error so a boundary further up the tree gets a chance to handle it. Just make sure the fallback template itself never throws—that would create an infinite loop of failures.
Other Lifecycle Hooks
onWillStart, onMounted, onWillUpdateProps, and onWillUnmount cover the hooks you'll reach for daily. OWL exposes a few more for less common, more advanced needs—know they exist so you recognize them in code you didn't write:
onWillRender()— runs immediately before a component's template function executes (parents before children). Rarely needed directly.onRendered()— runs immediately after the template function executes, but before the result is applied to the real DOM. Also rarely needed directly.onWillPatch()— runs just before an already-mounted component's DOM is updated (parents before children). Useful for reading DOM state—like a scroll position—before it changes.onPatched()— runs just after an already-mounted component's DOM has been updated (children before parents). This is the one you'll actually use sometimes: for example, re-measuring an element's size after its content changed.javascript onPatched(() => { // the DOM has just been updated with the latest state/props this.scrollToBottom(); });onWillDestroy()— always called when a component is being torn down, even if it never finished mounting. Use it for cleanup that must happen no matter what (as opposed toonWillUnmount, which only fires for components that were actually mounted).
For a junior developer, the practical rule is: reach for the four hooks from earlier in this chapter first, and only look at this second group when you have a specific, DOM-update-related problem to solve.
Common Pitfalls
Pitfall 1: Fetching Data in onMounted Instead of onWillStart
Fetching in onMounted renders the component empty first, then flickers once data arrives. Use onWillStart for data the component needs for its very first render.
Pitfall 2: Forgetting to Clean Up in onWillUnmount
Timers, window/document listeners, and third-party library instances started in onMounted keep running after the component is destroyed unless you clear them in onWillUnmount—a classic source of memory leaks.
Pitfall 3: Assuming the DOM Exists Too Early
this.someRef.el is only guaranteed to exist from onMounted onward. Accessing a useRef inside setup() or onWillStart will give you undefined.
Pitfall 4: Reloading Data on Every onWillUpdateProps Call
onWillUpdateProps runs whenever any prop changes, not just the one you care about. Always compare the old and new values (this.props.x !== nextProps.x) before re-fetching.
Pitfall 5: Forgetting an Error Boundary Anywhere in the Tree
Without an onError handler somewhere above it, a single throwing child component crashes the entire application, not just itself. Wrap risky subtrees (third-party widgets, anything rendering unpredictable data) in an ErrorBoundary component.
Exercises
Exercise 1: Fix the Flicker
Take a component that loads its data inside onMounted and move the fetch into onWillStart instead. Confirm in the browser that the "loading" flash disappears on first render.
Exercise 2: Plug the Leak
Write a component that starts a setInterval in onMounted without cleaning it up. Open the console, mount and unmount the component a few times, and observe the timer keeps firing. Then fix it with onWillUnmount.
Exercise 3: Selective Reloading
Build a component with two props, userId and theme. In onWillUpdateProps, only re-fetch user data when userId changes—not when theme changes. Add console.log calls to prove the fetch is skipped for theme-only updates.
Exercise 4: Build an Error Boundary
Write a Buggy component whose setup() throws if a crash prop is true. Wrap it in the ErrorBoundary component from this chapter and confirm that when crash is true, the rest of the page keeps working and shows the fallback message instead of a blank screen.
Summary
Lifecycle hooks are the foundation of robust OWL components. They let you:
onWillStart: Fetch critical data before renderingonMounted: Interact with the DOM and initialize third-party librariesonWillUpdateProps: React to prop changes and fetch new dataonWillUnmount: Clean up resources to prevent memory leaks
Master these hooks, and you'll be able to create components that are not only functional, but also performant and reliable.
TL;DR: onWillStart loads data before the first render, onMounted touches the DOM, onWillUpdateProps reacts to new props, and onWillUnmount cleans up — miss the last one and you leak memory.
Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch10_ex1. Installation instructions are in the repository README.
What's Next?
In the next chapter, we'll explore the power of custom hooks and how they can help you create reusable lifecycle patterns across your application.