All posts
Developer9 min read

Odoo OWL framework: building reactive JavaScript components for Odoo 16 and 17

OWL (Odoo Web Library) replaced the legacy Widget system as the JavaScript frontend framework starting in Odoo 14. This guide covers the component lifecycle, props and state, template syntax, event handling, service injection, and the key differences from the old Widget pattern.

What OWL Is#

OWL (Odoo Web Library) is the reactive JavaScript framework that powers the Odoo frontend from version 14 onwards. It replaces the legacy Widget system and is conceptually similar to React or Vue:

  • Component-based: UI is composed of reusable, self-contained components
  • Reactive state: components re-render when state or props change
  • Template-driven: uses an XML template syntax (QWeb for JS) co-located with the component
  • Service layer: shared functionality (RPC, notifications, dialogs) is accessed via injected services, not globals

OWL is open-source and maintained by Odoo S.A. You can use it independently of Odoo, but in practice it is tightly coupled to the Odoo web client infrastructure.


Component Anatomy#

A minimal OWL component consists of a JavaScript class and an XML template in the same module file.

javascript
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";

export class MyCounter extends Component {
    static template = "my_module.MyCounter";

    setup() {
        this.state = useState({ count: 0 });
    }

    increment() {
        this.state.count++;
    }
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
  <t t-name="my_module.MyCounter">
    <div class="my-counter">
      <span t-esc="state.count"/>
      <button t-on-click="increment">+</button>
    </div>
  </t>
</templates>

Key points:

  • / @odoo-module / tells the Odoo asset bundler this is an ES module
  • setup() is the lifecycle hook for initializing state and services
  • useState() creates a reactive proxy - mutations trigger re-renders
  • static template links the class to its XML template by fully-qualified name

Props#

Props are values passed from a parent component to a child. Declare expected props with the static props definition:

javascript
export class ProductCard extends Component {
    static template = "my_module.ProductCard";
    static props = {
        productId: Number,
        productName: String,
        optional: { type: Boolean, optional: true },
    };
}

In the parent template:

xml
<ProductCard productId="42" productName="'Odoo T-Shirt'" optional="true"/>

OWL validates props against the static props definition in development mode and logs warnings for mismatches.


Services#

Services provide shared functionality. Access them in setup() via useService():

javascript
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";

export class MyForm extends Component {
    static template = "my_module.MyForm";

    setup() {
        this.orm = useService("orm");
        this.notification = useService("notification");
    }

    async save() {
        try {
            await this.orm.write("sale.order", [this.props.recordId], { state: "sale" });
            this.notification.add("Order confirmed", { type: "success" });
        } catch (e) {
            this.notification.add("Save failed", { type: "danger" });
        }
    }
}

Common services:

  • orm: database read/write via JSON-RPC
  • notification: toast messages
  • dialog: programmatic dialog opening
  • action: trigger Odoo actions (open form views, run server actions)
  • router: URL navigation

Template Directives#

OWL templates use a subset of QWeb directives:

DirectivePurpose
t-escOutput escaped text
t-if / t-elseConditional rendering
t-foreach / t-asList rendering
t-on-clickEvent handler binding
t-modelTwo-way input binding
t-componentDynamic component name
t-slotSlot content injection
xml
<t t-foreach="state.items" t-as="item" t-key="item.id">
  <div t-if="item.active" t-on-click="() => this.select(item.id)">
    <span t-esc="item.name"/>
  </div>
</t>

Always include t-key on t-foreach loops - OWL uses it for efficient DOM diffing.


Registering a Component in the Odoo UI#

To add your component to an existing Odoo view, use the registry:

javascript
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { MyWidget } from "./my_widget";

registry.category("fields").add("my_widget", {
    component: MyWidget,
    supportedTypes: ["char", "many2one"],
});

Then reference it in an arch:

xml
<field name="my_field" widget="my_widget"/>

Key Differences from the Legacy Widget System#

ConceptLegacy WidgetOWL
Base classWidget (Backbone-based)Component (OWL)
State managementManual DOM / this.set()useState() reactive proxy
TemplateQWeb in XML with widget_nameSame QWeb, named module.ComponentName
Servicesthis._rpc(), global singletonsuseService("orm")
Lifecycleinit, start, destroysetup, onMounted, onWillUnmount
Module loadingAMD (odoo.define())ES modules (/ @odoo-module /)

Legacy widgets still work in Odoo 16/17 but are deprecated. New development should use OWL components.


Common Gotchas#

1. Missing t-key on loops causes unexpected re-renders. OWL cannot efficiently diff lists without stable keys.

2. Mutating props directly raises an error. Props are read-only; lift state to the parent or emit an event.

3. async setup() is not supported. Do async work in onMounted or in a useEffect pattern with a dedicated loading state.

4. / @odoo-module / comment must be the first line. If it appears after other code or imports, the bundler may not recognize it as an ES module.

5. Service access outside of setup() throws. Store the service reference in setup() and use it from methods.


ERPeek can answer "which OWL components are defined in this module?" and "what services does this component depend on?" across any Odoo 14+ codebase. See the contact page for a demo.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free