- 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
Frequently Asked Questions
When should I use useState vs useRef?
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.
How do I access the component's DOM element?
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.
Does OWL support async rendering?
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.