Part 2. Components · lesson 5 of 40
The anatomy of an OWL component
After this lesson you can: Write a component class with every part in the right place, and explain why setup() replaces the constructor.
Written against Odoo 19.0 and 18.0. Last checked 2026-10-09.
A component is a class extending Component with a few static fields and a
setup method. Here is one using every part worth knowing, before we take it
apart.
import { Component, useState } from "@odoo/owl";
import { ChildCard } from "./child_card";
export class InvoicePanel extends Component {
static template = "owl_course.InvoicePanel";
static components = { ChildCard };
static props = {
partnerId: { type: Number },
title: { type: String, optional: true },
onSelect: { type: Function, optional: true },
slots: { type: Object, optional: true },
};
static defaultProps = {
title: "Invoices",
};
setup() {
this.state = useState({ expanded: false, invoices: [] });
}
get hasInvoices() {
return this.state.invoices.length > 0;
}
toggle() {
this.state.expanded = !this.state.expanded;
}
}
static template
The name of an XML template, as a string. Not markup, not a function.
static template = "owl_course.InvoicePanel";
The convention module_name.ComponentName exists because template names are
global across the whole client. Two modules both registering Panel is a
collision that surfaces as one module mysteriously rendering the otherโs
markup.
static components
The child components this one may use in its template. OWL does not search globally; a component must be declared here or the template cannot reference it.
static components = { ChildCard };
In the template, the key is the tag name:
<ChildCard invoice="inv"/>
Forgetting this produces an error naming the unknown component, which is one of the clearer messages you will get.
static props
A description of what the parent may pass. In developer mode OWL validates against it and throws on a mismatch.
static props = {
partnerId: { type: Number },
title: { type: String, optional: true },
onSelect: { type: Function, optional: true },
};
Declaring props is optional and worth doing every time. The alternative is a
typo in a parent silently passing undefined into a child, which then fails
three layers away from the mistake. Props get a lesson of their own next.
static defaultProps
Values used when the parent omits an optional prop.
static defaultProps = { title: "Invoices" };
Only ever use primitives here. A default of [] or {} is a single object
shared by every instance of the component, so one instance mutating it changes
the default for all of them. Build those in setup instead.
setup()
Where instance work happens. All hooks must be called here, directly, not inside a condition or a callback.
setup() {
this.state = useState({ expanded: false, invoices: [] });
this.orm = useService("orm");
onWillStart(async () => {
this.state.invoices = await this.loadInvoices();
});
}
Why not a constructor
Three reasons.
OWL calls setup() from its own constructor after it has attached props,
env and the internal machinery. A constructor of yours runs before some of
that exists, so this.props may not be ready.
Hooks rely on OWL knowing which component is currently being set up. That is
established around setup() and nowhere else. Calling useState from a
constructor throws, and the message is not obvious.
And it is shorter. No super(...arguments) to forget.
// Wrong
constructor() {
super(...arguments);
this.state = useState({}); // throws
}
// Right
setup() {
this.state = useState({});
}
Getters
Plain JavaScript getters work and are the right home for derived values.
get hasInvoices() {
return this.state.invoices.length > 0;
}
In the template, no parentheses:
<div t-if="hasInvoices">...</div>
A getter re-runs on every render, which is exactly what you want. There is no
memoisation and in almost all cases you do not need any. If a getter is
genuinely expensive, cache in setup and invalidate deliberately rather than
reaching for a helper.
Methods and this
Methods are called from the template without binding:
<button t-on-click="toggle">Toggle</button>
OWL binds this for you here. Passing a method elsewhere, to setTimeout or to
a child as a callback, does not bind, and this will be undefined:
// Breaks
setTimeout(this.toggle, 1000);
// Works
setTimeout(() => this.toggle(), 1000);
Arrow functions in setup are the simplest habit.
What belongs in a component and what does not
The distinction that saves the most pain: a component displays and collects, the server decides.
A component that computes a tax, applies a discount rule or decides whether a record may be confirmed is a component that will disagree with Python, because Python will do it again when the record is saved. The user sees one number, the invoice carries another, and the difference is impossible to explain.
Call the server, display the result.
What is next
The template half: QWeb directives, what is available inside a template, and the ones that behave differently from the QWeb you know from reports.