Ad – 728×90
🦉 Lifecycle

OWL JS setup() & onMounted – Initialization and DOM Ready

setup() is the first method OWL calls on every component instance — it is where you register lifecycle hooks, inject services, and initialize reactive state. onMounted is the hook that fires after the component's DOM has been inserted into the document — the earliest moment you can safely interact with the real DOM. Together, these two are the most commonly used parts of the OWL lifecycle. This lesson covers both in depth with real patterns from production Odoo modules.

⏱️ 20 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 2 of 5

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 useState when you prefer setup() over class fields
JavaScript – setup() anatomy
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() { /* ... */ }
}
⚠️
Hooks must be called in setup() — not elsewhere

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:

JavaScript – class field vs setup() equivalents
// ── 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.

JavaScript – onMounted patterns
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();
    });
  }
}
Ad – 336×280

Common onMounted Patterns

DOM Measurement

JavaScript – measure after mount
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

JavaScript – scroll to element on mount
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

JavaScript – initialize a chart library
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

JavaScript – window event listener setup/teardown
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();
    }
  }
}
💡
Always pair onMounted with onWillUnmount for side effects

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.

JavaScript – useRef pattern
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);
    });
  }
}
Contextthis.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 inside setup() — calling them elsewhere throws.
  • Class fields that call hook functions (like state = useState({})) are equivalent to calling them in setup().
  • onMounted fires once after the first render inserts the DOM. this.el is available here.
  • Use onMounted for: DOM measurement, focus, scroll, global listeners, third-party library init.
  • useRef("name") gives you a reactive reference to a DOM element marked with t-ref="name" in the template. Access via this.myRef.el after mount.
  • Always pair onMounted side effects with cleanup in onWillUnmount.

🏋️ Exercise

Build a ResizablePanel component that demonstrates onMounted and useRef:

  1. Render a <div t-ref="panel"> with some content and a fixed min-height.
  2. In onMounted, use a ResizeObserver to watch the panel's size. When it resizes, update state.width and state.height.
  3. Display the current dimensions in the panel header.
  4. In onWillUnmount, call resizeObserver.disconnect().
  5. Add a "Expand" button that increases the panel's min-height via a state variable, triggering a size change that the ResizeObserver detects.

Frequently Asked Questions

Can I call super.setup() in a subclass? +

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.

Why can't I just use the constructor instead of setup()? +

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.

Is this.el available in setup()? +

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.

Can I call onMounted multiple times in setup()? +

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.