Skip to content

Odoo OWL Components: Frontend Development Guide

DeployMonkey Team · March 22, 2026 15 min read

What Is OWL?

OWL (Odoo Web Library) is Odoo's reactive JavaScript framework, used since Odoo 14. It is similar to React/Vue but designed specifically for Odoo. All frontend components in Odoo — form views, list views, kanban cards, buttons, dialogs — are OWL components.

OWL Component Basics

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

class EquipmentDashboard extends Component {
    static template = "equipment_tracking.Dashboard";
    static props = {};

    setup() {
        this.state = useState({
            count: 0,
            items: [],
            loading: true,
        });
        this.loadData();
    }

    async loadData() {
        const orm = this.env.services.orm;
        this.state.items = await orm.searchRead(
            "equipment.item",
            [],
            ["name", "status", "assigned_to"],
            { limit: 50 }
        );
        this.state.count = this.state.items.length;
        this.state.loading = false;
    }

    onClickRefresh() {
        this.state.loading = true;
        this.loadData();
    }
}

registry.category("actions").add("equipment_dashboard", EquipmentDashboard);

OWL Template (XML)

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="equipment_tracking.Dashboard">
        <div class="o_equipment_dashboard p-3">
            <h2>Equipment Dashboard</h2>
            <button class="btn btn-primary" t-on-click="onClickRefresh">
                Refresh
            </button>
            <p>Total: <t t-esc="state.count"/> items</p>

            <t t-if="state.loading">
                <div class="text-muted">Loading...</div>
            </t>
            <t t-else="">
                <table class="table">
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>Status</th>
                            <th>Assigned To</th>
                        </tr>
                    </thead>
                    <tbody>
                        <t t-foreach="state.items" t-as="item" t-key="item.id">
                            <tr>
                                <td><t t-esc="item.name"/></td>
                                <td>
                                    <span t-attf-class="badge #{item.status === 'available' ? 'bg-success' : 'bg-info'}">
                                        <t t-esc="item.status"/>
                                    </span>
                                </td>
                                <td><t t-esc="item.assigned_to[1] or '-'"/></td>
                            </tr>
                        </t>
                    </tbody>
                </table>
            </t>
        </div>
    </t>
</templates>

Key OWL Concepts

ConceptDescriptionExample
useStateReactive state objectthis.state = useState({ count: 0 })
useRefDOM element referencethis.inputRef = useRef("input")
useServiceAccess Odoo servicesthis.orm = useService("orm")
useEffectSide effects on state changeuseEffect(() => { ... }, () => [this.state.x])
onWillStartAsync setup before renderonWillStart(async () => { ... })
onMountedAfter first renderonMounted(() => { ... })

Using Odoo Services

import { useService } from "@web/core/utils/hooks";

class MyComponent extends Component {
    setup() {
        // ORM service (call Python models)
        this.orm = useService("orm");

        // RPC service (call controllers)
        this.rpc = useService("rpc");

        // Notification service
        this.notification = useService("notification");

        // Action service (navigate)
        this.action = useService("action");

        // Dialog service
        this.dialog = useService("dialog");
    }

    async fetchData() {
        // ORM call
        const records = await this.orm.searchRead("res.partner", [], ["name"]);

        // Controller call
        const result = await this.rpc("/my/api/endpoint", { param: "value" });

        // Show notification
        this.notification.add("Data loaded!", { type: "success" });

        // Navigate to record
        this.action.doAction({
            type: "ir.actions.act_window",
            res_model: "res.partner",
            res_id: 1,
            views: [[false, "form"]],
        });
    }
}

Registering Components

// As a client action (full-page component)
registry.category("actions").add("my_dashboard", MyDashboard);

// Then reference in XML:
// <record id="action_dashboard" model="ir.actions.client">
//     <field name="name">Dashboard</field>
//     <field name="tag">my_dashboard</field>
// </record>

// As a field widget
registry.category("fields").add("my_widget", { component: MyWidget });

// As a view component
registry.category("views").add("my_view", MyView);

Common Mistakes

  • Forgetting @odoo-module — Without the magic comment, Odoo does not load the JS file
  • Mutating state directly — Always use useState() for reactive state
  • Missing t-key in loops — Required for efficient DOM updates
  • Not using services — Direct XHR instead of useService('rpc') bypasses Odoo's auth and error handling

AI + OWL

Claude Code generates OWL components from descriptions. Describe the dashboard, widget, or interaction you need, and it creates the component class, template, and registration code with correct Odoo 19 syntax.