What setup() Does
setup() is a special class method that OWL calls synchronously during component construction, before any render. It is the designated place for all initialization logic. This includes:
- Calling lifecycle hook functions (
onWillStart,onMounted, etc.) - Injecting services via
useService() - Calling built-in hooks like
useRef(),useEnv(),useStore() - Calling custom hooks
- Initializing
useStatewhen you prefersetup()over class fields
import {
Component, xml, useState,
onWillStart, onMounted, onWillUnmount,
useRef, useEnv
} from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
class DataTable extends Component {
setup() {
// 1. Get services (Odoo context)
this.orm = useService("orm");
this.action = useService("action");
this.notification = useService("notification");
// 2. Initialize reactive state
this.state = useState({
records: [],
isLoading: true,
sortField: "name",
sortAsc: true,
page: 1,
});
// 3. Get DOM refs
this.tableRef = useRef("table");
// 4. Register lifecycle hooks
onWillStart(() => this.loadData());
onMounted(() => {
this.initResizeObserver();
});
onWillUnmount(() => {
this.resizeObserver?.disconnect();
});
}
async loadData() { /* ... */ }
initResizeObserver() { /* ... */ }
}
OWL hook functions (onMounted, useService, useRef, etc.) track which component they belong to using a global "current component" reference that is only set during setup(). Calling them in an event handler, a regular method, or an async callback will throw: "No component is being set up." Always call hooks synchronously inside setup().
Class Fields vs setup()
Class fields that call hook functions are syntactic sugar — they run before setup() but inside the same "current component" context. Both patterns are valid:
// ── Class field style ───────────────────────────────────────────────
class ComponentA extends Component {
state = useState({ count: 0 }); // class field
inputRef = useRef("myInput"); // class field
setup() {
this.orm = useService("orm"); // services still need setup()
onMounted(() => { ... });
}
}
// ── Full setup() style ──────────────────────────────────────────────
class ComponentB extends Component {
setup() {
this.state = useState({ count: 0 });
this.inputRef = useRef("myInput");
this.orm = useService("orm");
onMounted(() => { ... });
}
}
// Both are equivalent. Mix and match — use class fields for state,
// setup() for services and lifecycle hooks.
onMounted – The DOM-Ready Hook
onMounted fires once, immediately after the component's first render is inserted into the document. At this point, this.el is a real DOM element you can query, measure, and manipulate.
import { Component, xml, useState, onMounted, useRef } from "@odoo/owl";
class AutoFocusInput extends Component {
static template = xml`
<div>
<input t-ref="input" type="text" placeholder="I auto-focus on mount"/>
</div>
`;
inputRef = useRef("input");
setup() {
onMounted(() => {
// this.inputRef.el is the real <input> DOM element
this.inputRef.el?.focus();
});
}
}
Common onMounted Patterns
DOM Measurement
class ResponsiveChart extends Component {
state = useState({ width: 0, height: 0 });
setup() {
onMounted(() => {
// getBoundingClientRect() requires the element to be in the DOM
const rect = this.el.getBoundingClientRect();
this.state.width = rect.width;
this.state.height = rect.height;
});
}
}
Scroll Management
class ChatWindow extends Component {
static template = xml`
<div class="chat-window" t-ref="window">
<div t-foreach="props.messages" t-as="msg" t-key="msg.id"
class="message" t-esc="msg.text"/>
</div>
`;
windowRef = useRef("window");
setup() {
onMounted(() => {
// Scroll to the bottom after first render
this.scrollToBottom();
});
}
scrollToBottom() {
const el = this.windowRef.el;
if (el) el.scrollTop = el.scrollHeight;
}
}
Third-Party Library Initialization
class SalesChart extends Component {
static template = xml`
<div>
<canvas t-ref="canvas" width="600" height="300"></canvas>
</div>
`;
canvasRef = useRef("canvas");
chart = null;
setup() {
onMounted(() => {
// Chart.js (or any lib) needs the real DOM canvas element
this.chart = new Chart(this.canvasRef.el, {
type: "line",
data: { labels: props.labels, datasets: [{ data: props.data }] },
});
});
onWillUnmount(() => {
this.chart?.destroy(); // ← always clean up third-party libraries
});
}
}
Global Event Listeners
class KeyboardShortcuts extends Component {
setup() {
// Store the bound reference so we can remove the exact same function
this._onKeyDown = this.handleGlobalKey.bind(this);
onMounted(() => {
window.addEventListener("keydown", this._onKeyDown);
});
onWillUnmount(() => {
// Remove the exact same reference
window.removeEventListener("keydown", this._onKeyDown);
});
}
handleGlobalKey(ev) {
if ((ev.ctrlKey || ev.metaKey) && ev.key === "s") {
ev.preventDefault();
this.save();
}
}
}
Every global event listener, timer, ResizeObserver, IntersectionObserver, WebSocket connection, or third-party library instance you create in onMounted must be destroyed in onWillUnmount. If you do not clean up, these keep running after the component is removed, causing memory leaks and subtle bugs. This pairing is one of the most important patterns in OWL development.
useRef() – Accessing DOM Elements
useRef(name) creates a ref that OWL fills with the DOM element that has t-ref="name" in the template. The element is set when the component mounts and cleared when it unmounts.
import { Component, xml, onMounted, useRef } from "@odoo/owl";
class VideoPlayer extends Component {
static template = xml`
<div>
<video t-ref="video" src="/assets/intro.mp4" controls></video>
<button t-on-click="play">▶ Play</button>
<button t-on-click="pause">⏸ Pause</button>
</div>
`;
videoRef = useRef("video");
play() { this.videoRef.el?.play(); }
pause() { this.videoRef.el?.pause(); }
setup() {
onMounted(() => {
// Safe to access videoRef.el here
console.log("Video duration:", this.videoRef.el?.duration);
});
}
}
| Context | this.myRef.el value |
|---|---|
| During setup() | null — DOM not yet created |
| During onWillStart() | null — still before first render |
| During onMounted() | ✅ The real DOM element |
| During event handlers (after mount) | ✅ The real DOM element |
| During onWillUnmount() | ✅ Still the real DOM element |
| After onWillUnmount() | null — element removed |
📋 Summary
setup()runs synchronously during construction — before any render. Use it for all hook registration, service injection, and initialization.- Hook functions (
onMounted,useService,useRef) must be called insidesetup()— calling them elsewhere throws. - Class fields that call hook functions (like
state = useState({})) are equivalent to calling them insetup(). onMountedfires once after the first render inserts the DOM.this.elis available here.- Use
onMountedfor: DOM measurement, focus, scroll, global listeners, third-party library init. useRef("name")gives you a reactive reference to a DOM element marked witht-ref="name"in the template. Access viathis.myRef.elafter mount.- Always pair
onMountedside effects with cleanup inonWillUnmount.
🏋️ Exercise
Build a ResizablePanel component that demonstrates onMounted and useRef:
- Render a
<div t-ref="panel">with some content and a fixed min-height. - In
onMounted, use aResizeObserverto watch the panel's size. When it resizes, updatestate.widthandstate.height. - Display the current dimensions in the panel header.
- In
onWillUnmount, callresizeObserver.disconnect(). - Add a "Expand" button that increases the panel's
min-heightvia a state variable, triggering a size change that the ResizeObserver detects.
Frequently Asked Questions
Yes — if you subclass a component and both parent and child define setup(), the child should call super.setup() to ensure the parent's setup logic (hook registrations, service injections) runs. OWL does not call super.setup() automatically, so forgetting it will silently skip the parent's initialization.
OWL's hook system relies on a global "current component" context that is set during setup() — not the constructor. If you call useService() or onMounted() in the constructor, the context is not set and they throw. Additionally, setup() enables custom hooks — reusable functions that call other hooks internally. This composability is not possible with constructors because you cannot inject into an arbitrary constructor call stack in the same way.
No — this.el is null during setup() because the component has not rendered yet. The DOM does not exist until after the first render and insertion. this.el becomes available in onMounted and stays available until after onWillUnmount. If you try to access this.el in setup(), you get null — no error, just no element.
Yes — each call to onMounted(callback) registers an additional callback. All registered callbacks fire when the component mounts, in the order they were registered. This is intentional — it allows custom hooks to independently register onMounted callbacks without knowing what else is registered. All callbacks run before the parent's onMounted fires.