OWL Components Inside Odoo: the Anatomy
Odoo's front end is built on OWL, its own React-like framework. A component is a JavaScript class extending Component, paired with an XML template. Understanding the four pieces lets you build custom widgets and dashboards.
import { Component, useState } from "@odoo/owl";
class Counter extends Component {
static template = "my_module.Counter"; // links to XML template
static props = { start: { type: Number, optional: true } };
setup() { // runs once at creation
this.state = useState({ value: this.props.start || 0 });
}
increment() { this.state.value++; } // mutating state re-renders
}
| Piece | Role |
|---|---|
static template | XML that defines the markup |
setup() | constructor replacement — hooks, state |
props | read-only data from the parent |
useState | reactive state — changes trigger re-render |
Reactivity rule: only changes to a useState object re-render the component. Assigning to a plain this.x updates the value but the UI won't notice. And props flow one way — a child must never mutate its props; it emits an event and lets the parent change the data.
🏋️ Practical Exercise
- Define an OWL component class.
- Add a static QWeb template.
- Use reactive state with
useState. - Handle a click with
t-on-click. - Render props from a parent.
🔥 Challenge Exercise
Build a small OWL counter component with reactive state (via useState), a template using t-esc and t-on-click, and props passed from a parent. Explain the component lifecycle and how OWL’s reactivity re-renders on state change.
- 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:
/** @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;
}
}
<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:
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
| Hook | When 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 |
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);
});
}
Props Down, Events Up
Data flows down via props; user interactions flow up via custom events:
// 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;
}
}
<!-- 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:
<!-- 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>
- Components link to templates via
static template = 'module.TemplateName' useState()creates reactive state — mutate properties, never replace the proxy object- Use
onMountedfor initial data loading andonWillUnmountfor cleanup - Props flow down; callbacks (or events) flow up — never mutate parent state directly
Interview Questions
- How do you define an OWL component?
- What is
useState? - How does OWL handle events in templates?
- What are props in OWL?
- What are OWL lifecycle hooks?
Related Topics
FAQ
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.
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.
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.

