Este artigo está atualmente disponível apenas em inglês. Tradução em breve.
Odoo OWL Framework: Components, Props, Hooks, and Reactivity
OWL — Odoo Web Library — is the JavaScript framework that powers Odoo's web client since version 16. It's a homegrown reactive component framework with intentional similarities to React (components, props, hooks) and Vue (template syntax, reactivity), but with its own conventions tuned for Odoo's needs. If you're writing custom views, snippet options, POS extensions, or any front-end Odoo customization, OWL is non-negotiable.
This article covers the OWL concepts you actually need to ship production code, with examples from real Odoo modules.
Key Takeaways
- OWL components are TypeScript/JS classes extending
Componentwith a statictemplate- Props are typed via
static propsdeclarations — strict type checking at runtimeuseStateprovides reactive state; mutations trigger re-rendersonMounted,onWillStart,onPatched, etc. are lifecycle hooksuseServiceconnects to Odoo's service layer (ORM, dialogs, notifications, router)- Templates use XML with
t-on-,t-att-,t-foreach-directives — OWL's QWeb dialect- Components register in
web.assets_backendor scoped bundles based on use case
OWL component anatomy
A minimal OWL component:
import { Component } from "@odoo/owl";
export class HelloWorld extends Component {
static template = "my_module.HelloWorld";
static props = {
name: { type: String, optional: true },
};
}
With its template (separate XML file):
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="my_module.HelloWorld">
<div class="hello">
Hello, <t t-esc="props.name or 'World'"/>!
</div>
</t>
</templates>
Register the component (typically via a service or as part of an action) and it's ready.
Props and prop validation
OWL enforces prop types at runtime in dev mode:
static props = {
user: { type: Object, shape: { name: String, age: Number } },
onSave: Function,
items: { type: Array, element: { type: Object } },
visible: { type: Boolean, optional: true },
"*": true, // allow extra props (less strict)
};
Unrecognized props or wrong types throw in dev. Production silently allows for backward compatibility, but the dev catch is invaluable.
Reactive state with useState
import { Component, useState } from "@odoo/owl";
export class Counter extends Component {
static template = "my_module.Counter";
setup() {
this.state = useState({ count: 0 });
}
increment() {
this.state.count++; // triggers re-render
}
}
<t t-name="my_module.Counter">
<button t-on-click="increment">
Count: <t t-esc="state.count"/>
</button>
</t>
Mutations to objects/arrays in state are tracked deeply. Unlike React, you don't need spread syntax.
Lifecycle hooks
| Hook | When | Use case |
|---|---|---|
onWillStart | Before first render, can be async | Fetch initial data |
onMounted | After component is in DOM | DOM manipulation, event listeners |
onWillUpdateProps | Before re-render due to prop change | Sync derived state |
onPatched | After re-render | DOM-based side effects |
onWillUnmount | Before removal from DOM | Cleanup listeners, timers |
onError | When child throws | Error boundary |
import { Component, useState, onWillStart, onMounted, onWillUnmount } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class CustomerList extends Component {
static template = "my_module.CustomerList";
setup() {
this.orm = useService("orm");
this.state = useState({ customers: [], loading: true });
this._timer = null;
onWillStart(async () => {
this.state.customers = await this.orm.searchRead(
"res.partner",
[["is_company", "=", true]],
["id", "name", "email"]
);
this.state.loading = false;
});
onMounted(() => {
this._timer = setInterval(() => this.refresh(), 30000);
});
onWillUnmount(() => {
clearInterval(this._timer);
});
}
async refresh() {
this.state.customers = await this.orm.searchRead(
"res.partner",
[["is_company", "=", true]],
["id", "name", "email"]
);
}
}
useService — accessing Odoo's services
useService is the canonical way to access Odoo platform features:
import { useService } from "@web/core/utils/hooks";
setup() {
this.orm = useService("orm"); // ORM operations
this.action = useService("action"); // open actions, navigate
this.notification = useService("notification"); // toast notifications
this.dialog = useService("dialog"); // open dialogs
this.user = useService("user"); // current user info
this.router = useService("router"); // URL state
this.rpc = useService("rpc"); // raw RPC call
}
// Use them
await this.orm.create("res.partner", { name: "Acme" });
this.notification.add("Customer created", { type: "success" });
this.dialog.add(SomeDialogComponent, { props });
Template directives
OWL templates extend QWeb with reactive directives:
| Directive | Purpose | Example |
|---|---|---|
t-esc | Escaped text | <span t-esc="state.name"/> |
t-out | Raw HTML (careful!) | <div t-out="state.html"/> |
t-att-X | Attribute binding | <input t-att-disabled="state.locked"/> |
t-attf-X | Format string attr | <div t-attf-class="status-{{state.status}}"/> |
t-on-X | Event handler | <button t-on-click="save">Save</button> |
t-if, t-else | Conditional render | <div t-if="state.error">Error!</div> |
t-foreach | Loop | <li t-foreach="state.items" t-as="item" t-key="item.id"> |
t-ref | Refs to DOM | <input t-ref="searchInput"/> |
t-component | Render child component | <t t-component="MyChild" propName="value"/> |
t-slot | Slot content | <t t-slot="default"/> |
Slots — composition
Slots let parent components pass content into child components:
<!-- Card component template -->
<t t-name="my_module.Card">
<div class="card">
<div class="card-header">
<t t-slot="header"/>
</div>
<div class="card-body">
<t t-slot="default"/>
</div>
</div>
</t>
<!-- Usage -->
<Card>
<t t-set-slot="header">My Title</t>
<p>Body content here</p>
</Card>
Slots are like React's children + named slots in one mechanism.
Refs to DOM
import { useRef, onMounted } from "@odoo/owl";
setup() {
this.searchInput = useRef("searchInput");
onMounted(() => {
this.searchInput.el.focus();
});
}
<input t-ref="searchInput" type="text"/>
Custom hooks
Build reusable logic via custom hooks (functions starting with use):
import { useState, onMounted, onWillUnmount } from "@odoo/owl";
export function useWindowSize() {
const state = useState({ width: window.innerWidth, height: window.innerHeight });
function update() {
state.width = window.innerWidth;
state.height = window.innerHeight;
}
onMounted(() => window.addEventListener("resize", update));
onWillUnmount(() => window.removeEventListener("resize", update));
return state;
}
// Use it in any component
setup() {
this.size = useWindowSize();
}
Comparison to React
| Concept | OWL | React |
|---|---|---|
| Component | class extends Component | function or class extends Component |
| State | useState({...}) deep reactive | useState(value) shallow + setter |
| Effect | onMounted, onWillUnmount | useEffect(() => {...}, [deps]) |
| Memoization | Computed via getters in setup | useMemo, useCallback |
| Refs | useRef("name") + template t-ref | useRef() + JSX ref= |
| Children | t-slot (named slots) | props.children |
| Template | XML with t- directives | JSX (JavaScript expressions) |
Common patterns in Odoo customizations
Override an existing component
import { patch } from "@web/core/utils/patch";
import { ListController } from "@web/views/list/list_controller";
patch(ListController.prototype, {
setup() {
super.setup();
// your additions
},
onCustomAction() {
// your new method
},
});
Register a field widget
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
class MyCustomField extends Component {
static template = "my_module.MyCustomField";
static props = { ...standardFieldProps };
}
registry.category("fields").add("my_custom", {
component: MyCustomField,
});
Then in XML view: <field name="my_field" widget="my_custom"/>.
Asset registration
Components must register in an asset bundle:
# __manifest__.py
{
'name': 'My Module',
'assets': {
'web.assets_backend': [
'my_module/static/src/components/customer_list/*',
],
},
}
For Odoo 19's split bundles, choose the right one:
web.assets_backend— admin/internal user featuresweb.assets_frontend— public websitepoint_of_sale.assets— POS-specificwebsite.assets_edit_frontend— website editor
Frequently Asked Questions
Is OWL similar enough to React that React developers can pick it up quickly?
Yes, with a 1-2 day ramp. The component model and hooks pattern translate directly. The biggest mental shift is the template language (XML directives instead of JSX) and the deep reactivity (mutate state directly instead of immutable updates). Production-ready OWL code from a React developer comes within a sprint.
Can I use OWL outside of Odoo?
Technically yes — OWL is open source on GitHub. But its services and asset pipeline are tightly coupled to Odoo. For non-Odoo projects, React or Vue make more sense.
How do I debug OWL components?
Open the browser devtools, switch to dev mode in Odoo (?debug=1), and OWL provides a component tree inspector. The reactivity system logs trigger sources in the console when verbose logging is enabled. Standard JS debugger statements work as usual.
Does OWL support TypeScript?
Yes, increasingly so in newer Odoo versions. Types are provided for components, hooks, and services. Most Odoo core code is still JS, but you can write TypeScript modules and they compile via the asset pipeline.
What about server-side rendering?
OWL is a client-side framework. Odoo's website module uses QWeb (server-rendered) for public pages and OWL for editor and admin. There's no built-in OWL SSR; for SEO-critical content, use server-rendered QWeb templates.
OWL is the front-end framework you need for any production Odoo customization. ECOSIRE's Odoo developer hire service places engineers fluent in OWL who can ship custom widgets, screens, and integrations. See our Odoo customization service for module development including front-end work, or browse our Odoo modules catalog for examples of well-structured OWL components.
Escrito por
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Transforme seu negócio com o Odoo ERP
Implementação, personalização e suporte especializado do Odoo para agilizar suas operações.
Artigos Relacionados
Como adicionar um botão personalizado a uma visualização de formulário Odoo (2026)
Adicione botões de ação personalizados às visualizações de formulário do Odoo 19: método de ação Python, herança de visualização, visibilidade condicional, caixas de diálogo de confirmação. Testado em produção.
Como adicionar um campo personalizado no Odoo sem Studio (2026)
Adicione campos personalizados por meio de módulo personalizado no Odoo 19: herança de modelo, extensão de visualização, campos computados, decisões de loja/não loja. Código primeiro, controlado por versão.
Como adicionar um relatório personalizado no Odoo usando layout externo
Crie um relatório PDF de marca no Odoo 19 usando web.external_layout: modelo QWeb, formato de papel, vinculação de ação. Com logotipo impresso + substituições de rodapé.