Ad – 728×90
🦉 OWL in Odoo

OWL Components in Odoo – Writing and Registering Components

OWL components are ES6 classes that extend Component. They have a linked XML template, reactive state, and a lifecycle managed by OWL. In Odoo, you write components to build custom field widgets, systray items, dashboard panels, or entire views.

⏱️ 25 min 🎯 Intermediate 📅 Updated 2026
What you'll learn:
  • Component class structure: template, props, state
  • Reactive state with useState()
  • Lifecycle hooks: onMounted, onWillUnmount, onWillUpdateProps
  • Parent-child communication: props down, events up
  • Using slots for composable components

Component Structure

Every OWL component has a JavaScript class and an XML template. The class links to the template via static template:

JavaScript
/** @odoo-module **/
import { Component, useState, onMounted } from '@odoo/owl';
import { useService } from '@web/core/utils/hooks';


export class BookCounter extends Component {
    static template = 'my_module.BookCounter';

    // Prop validation (optional but recommended)
    static props = {
        modelName: { type: String },
        domain: { type: Array, optional: true },
    };

    setup() {
        // Services
        this.orm = useService('orm');

        // Reactive state — changes trigger re-render
        this.state = useState({
            count: 0,
            loading: true,
        });

        // Lifecycle hook — runs after first render
        onMounted(async () => {
            await this.loadCount();
        });
    }

    async loadCount() {
        this.state.loading = true;
        this.state.count = await this.orm.searchCount(
            this.props.modelName,
            this.props.domain || [],
        );
        this.state.loading = false;
    }
}
XML (OWL template)
<templates>
  <t t-name="my_module.BookCounter">
    <div class="book-counter">
      <t t-if="state.loading">
        <span>Loading...</span>
      </t>
      <t t-else="">
        <span class="count"><t t-esc="state.count"/></span>
        <span class="label"><t t-esc="props.modelName"/> records</span>
      </t>
    </div>
  </t>
</templates>

Reactive State with useState

useState() wraps a plain object in a reactive proxy. Any mutation to its properties triggers a re-render of the component and its affected children:

JavaScript
this.state = useState({ items: [], filter: '' });

// Mutating state triggers re-render
this.state.filter = 'active';
this.state.items = await this.orm.searchRead(...);

// Don't replace the reactive object itself — mutate properties
// WRONG: this.state = useState({ ...newData })
// RIGHT: Object.assign(this.state, newData)

Lifecycle Hooks

HookWhen it runs
onMounted(fn)After first render, DOM is available
onWillUnmount(fn)Before component is removed from DOM — cleanup here
onWillUpdateProps(fn)Before props change — re-fetch data if needed
onPatched(fn)After every render (including re-renders)
onWillRender(fn)Before each render
JavaScript
import { onMounted, onWillUnmount, onWillUpdateProps } from '@odoo/owl';

setup() {
    this.state = useState({ records: [] });

    onMounted(async () => {
        await this.loadRecords(this.props.domain);
    });

    onWillUpdateProps(async (nextProps) => {
        if (nextProps.domain !== this.props.domain) {
            await this.loadRecords(nextProps.domain);
        }
    });

    onWillUnmount(() => {
        clearInterval(this._refreshInterval);
    });
}
Ad – 728×90

Props Down, Events Up

Data flows down via props; user interactions flow up via custom events:

JavaScript
// Child component emits event
export class BookCard extends Component {
    static template = 'my_module.BookCard';
    static props = { book: Object };

    onSelect() {
        this.props.onBookSelected(this.props.book);  // callback prop
    }
}

// Parent passes callback as prop
export class BookList extends Component {
    static template = 'my_module.BookList';
    static components = { BookCard };  // register sub-components

    setup() {
        this.state = useState({ selectedBook: null });
    }

    onBookSelected(book) {
        this.state.selectedBook = book;
    }
}
XML (OWL template)
<!-- Parent template -->
<t t-foreach="state.books" t-as="book">
  <BookCard
    book="book"
    onBookSelected="(b) => this.onBookSelected(b)"/>
</t>

Slots

Slots allow parent components to inject content into a child's template:

XML (OWL template)
<!-- Card component template with a slot -->
<t t-name="my_module.Card">
  <div class="card">
    <div class="card-header"><t t-esc="props.title"/></div>
    <div class="card-body">
      <t t-slot="default"/>  <!-- content from parent -->
    </div>
  </div>
</t>

<!-- Parent usage -->
<Card title="'My Book'">
  <p>This content goes into the slot.</p>
  <BookCounter modelName="'library.book'"/>
</Card>
Key takeaways:
  • Components link to templates via static template = 'module.TemplateName'
  • useState() creates reactive state — mutate properties, never replace the proxy object
  • Use onMounted for initial data loading and onWillUnmount for cleanup
  • Props flow down; callbacks (or events) flow up — never mutate parent state directly

Frequently Asked Questions

When should I use useState vs useRef?

useState() for data that should trigger re-renders when changed (counts, records, flags). useRef() for mutable values that should NOT trigger re-renders — DOM element references, timers, or caches. Mutating a ref doesn't cause a re-render; mutating state does.

How do I access the component's DOM element?

Use useRef() and attach it with t-ref: declare this.root = useRef('root') in setup, then add t-ref="root" to the element in the template. After mounting, this.root.el is the DOM element. Only access it in onMounted or later — it's null before the first render.

Does OWL support async rendering?

Yes — OWL supports async components via willStart() (an old API) and async lifecycle hooks. Modern approach: use onMounted with await for data loading, but render a loading state immediately so the UI isn't blocked. OWL batches state mutations within the same microtask to minimize re-renders.