The Minimal Component
Every OWL component has exactly two required pieces: a class that extends Component, and a static template property that holds the QWeb XML template string.
import { Component, xml } from "@odoo/owl";
class Greeting extends Component {
static template = xml`
<div>Hello, World!</div>
`;
}
export default Greeting;
The xml helper is a tagged template literal that registers the QWeb template string with the OWL template engine and returns a unique template name. Without it, OWL would not know how to compile or look up your template.
xml is a tagged template literal, not just a string
OWL needs to compile QWeb templates exactly once. The xml`` tag records the template under a stable internal key and returns that key. Passing a plain string would not register it. Think of xml`` as "compile this template and give me a handle to it."
Full Component Anatomy
A real component uses several static class properties. Here is every one you will encounter, with a brief description of each:
import { Component, xml, useState } from "@odoo/owl";
import ChildComponent from "./ChildComponent";
class UserCard extends Component {
// ── Static properties ──────────────────────────────────────────────
// 1. Template (required) — the QWeb XML for this component
static template = xml`
<div class="user-card" t-att-class="{ active: state.isActive }">
<h2 t-esc="props.name"/>
<p t-esc="props.email"/>
<ChildComponent count="state.clicks"/>
<button t-on-click="toggle">Toggle</button>
</div>
`;
// 2. components — child components used in the template (must be registered here)
static components = { ChildComponent };
// 3. props — prop type declarations (optional but strongly recommended)
static props = {
name: { type: String },
email: { type: String, optional: true },
};
// 4. defaultProps — default values for optional props
static defaultProps = {
email: "not provided",
};
// ── Instance members ───────────────────────────────────────────────
// Reactive state — changes here trigger re-render
state = useState({ isActive: false, clicks: 0 });
// Instance method (event handler)
toggle() {
this.state.isActive = !this.state.isActive;
this.state.clicks++;
}
}
Static Properties Reference
| Static Property | Type | Required? | Purpose |
|---|---|---|---|
template | template key (from xml``) | Yes | The QWeb template to render for this component |
components | { [name]: ComponentClass } | Only if template uses sub-components | Registers child components so OWL can instantiate them by name in the template |
props | props definition object | No (recommended) | Declares and type-checks props passed to this component. OWL throws in dev mode if a prop violates the spec. |
defaultProps | plain object | No | Default values for optional props. Merged with this.props at construction time. |
Mounting a Component
To render the root component of your app into the DOM, use mount(). This is only called once — for the root. All child components are instantiated by OWL internally when the parent template renders them.
import { mount } from "@odoo/owl";
import App from "./App";
// Mount to a DOM node
mount(App, document.getElementById("app"));
// mount() also accepts an options object (second positional arg replaced by options in OWL 2)
mount(App, document.getElementById("app"), {
props: { title: "My OWL App" }, // props to pass to the root
dev: true, // enable dev mode (prop checks, warnings)
translateFn: (str) => str, // i18n translation function
});
mount() creates the root component instance, sets up the rendering environment, and performs the first render. Never instantiate a component with new MyComponent() — OWL manages component lifecycles internally. Only call mount() for the single root component of your app.
Component Trees
OWL applications are a tree of components, with one root component at the top. Parent components pass data down via props; child components communicate back via custom events. Each component manages its own template and state.
import { Component, xml, useState } from "@odoo/owl";
// ── Child component ────────────────────────────────────────────────
class TodoItem extends Component {
static template = xml`
<li>
<span t-esc="props.text"/>
<button t-on-click="() => props.onDelete(props.id)">✕</button>
</li>
`;
static props = {
id: { type: Number },
text: { type: String },
onDelete: { type: Function }, // callback prop
};
}
// ── Parent component ───────────────────────────────────────────────
class TodoList extends Component {
static components = { TodoItem };
static template = xml`
<div>
<h1>Todos</h1>
<ul>
<TodoItem
t-foreach="state.todos"
t-as="todo"
t-key="todo.id"
id="todo.id"
text="todo.text"
onDelete.bind="deleteTodo"
/>
</ul>
</div>
`;
state = useState({
todos: [
{ id: 1, text: "Learn OWL JS" },
{ id: 2, text: "Build an Odoo module" },
],
});
deleteTodo(id) {
const idx = this.state.todos.findIndex(t => t.id === id);
if (idx !== -1) this.state.todos.splice(idx, 1);
}
}
static components
If your template uses <TodoItem .../> but TodoItem is not listed in static components = { TodoItem }, OWL will throw an error at render time. Every component used in a template must be registered by its parent.
Passing Props
Props are passed in the template using attribute-like syntax. There are three syntaxes depending on the value type:
<!-- 1. String literal — value in quotes is a plain string -->
<UserCard name="'Alice'" />
<!-- 2. JavaScript expression — quotes contain any JS expression -->
<UserCard name="user.firstName + ' ' + user.lastName" />
<!-- 3. Shorthand for boolean true -->
<UserCard isAdmin /> <!-- equivalent to isAdmin="true" -->
<!-- 4. Bound method (preserves 'this') -->
<UserCard onSave.bind="saveUser" />
name="Alice" is a JavaScript expression — it evaluates the variable Alice. name="'Alice'" is a string literal because the inner quotes make it a JS string. This trips up almost every OWL beginner at least once.
Accessing Props and State in Templates
Inside a QWeb template, the component instance is the implicit scope. You access props directly and state directly (without this):
<!-- In template: no 'this.' needed -->
<div>
<span t-esc="props.username"/>
<span t-esc="state.count"/>
<span t-esc="computedValue"/> <!-- JS getter on the class -->
</div>
class MyComponent extends Component {
state = useState({ count: 0 });
get computedValue() {
// 'this' works in getters and methods
return this.state.count * 2;
}
increment() {
this.state.count++; // direct mutation — OWL detects it
}
}
How OWL Re-renders a Component
OWL uses a virtual DOM (vdom) diffing algorithm. When reactive state changes, OWL queues a re-render for the affected component, performs the diff between old and new vdom, and applies only the minimal DOM patches needed. You never call a render function manually.
| Trigger | What happens |
|---|---|
Mutation on a useState object | OWL schedules a micro-task re-render for the owning component |
| Parent re-renders and passes new props | Child re-renders automatically if props changed |
| Child receives same prop values | Child does NOT re-render (shallow prop comparison) |
Manual this.render() | Forces a re-render — almost never needed |
Naming Conventions
| Thing | Convention | Example |
|---|---|---|
| Component class | PascalCase | UserCard, TodoList, InvoiceLine |
| Template tag in parent template | PascalCase (same as class name) | <UserCard .../> |
| Component file | PascalCase or kebab-case | UserCard.js or user-card.js |
| Props | camelCase | userName, onDelete, isActive |
| Event handler methods | verb + noun | handleClick, toggleMenu, submitForm |
| State fields | camelCase | isLoading, errorMessage |
Components in Odoo Modules
In real Odoo modules, components are not mounted with mount() — they are registered in Odoo's component registry instead. OWL picks them up from the registry when the backend renders a view.
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
class MyWidget extends Component {
static template = xml`
<div class="my-widget">
<span t-esc="props.record.data.name"/>
</div>
`;
static props = {
record: Object,
};
}
// Register in the "fields" registry to use as a custom field widget
registry.category("fields").add("my_widget", {
component: MyWidget,
});
export default MyWidget;
Anatomy of an OWL Component
An OWL component is a JavaScript class extending Component, tied to an XML template. This class-plus-template pairing is the fundamental building block of every OWL app.
import { Component, xml, useState } from "@odoo/owl";
class Greeting extends Component {
static template = xml`
<div>Hello, <t t-esc="props.name"/>! Clicked <t t-esc="state.n"/>
<button t-on-click="inc">+</button>
</div>`;
static props = ["name"];
setup() {
this.state = useState({ n: 0 }); // reactive
}
inc() { this.state.n++; } // mutation re-renders
}
| Part | Purpose |
|---|---|
static template | the XML markup (inline via xml`` or by name) |
static props | declares expected inputs from parent |
setup() | init logic + hooks — the constructor replacement |
Why setup() and not constructor()? Hooks like useState and onMounted need the component's internal context, which OWL wires up right before calling setup. Put hook calls anywhere else and they throw. Template expressions read from this — so props, state, and your methods are all reachable as props.x, state.y.
🏋️ Practical Exercise
Build a three-component tree to practice the concepts from this lesson:
- Create a
Badgecomponent that accepts alabel(String) prop and acolor(String, optional, default"blue") prop. Render<span class="badge">{label}</span>with an inline style for the color. - Create a
UserCardcomponent that acceptsname,role, andtags(Array) props. Uset-foreachto render a<Badge>for each tag. - Create a
UserListroot component with a hard-coded array of 3 users inuseState. Render a<UserCard>for each user. Add a "Remove" button that splices the user out of the array.
Focus on: static components at each level, correct prop passing syntax, and prop type declarations.
🔥 Challenge Exercise
Create a component with a setup() that initializes data, a template that renders it, and a child registered via static components. Explain the component class structure and the role of setup() (OWL’s constructor-like hook).
📋 Summary
- Every OWL component is a class that
extends Componentwith astatic template = xml`...`. static componentsregisters child components used in the template — required for every sub-component.static propsdeclares and type-checks incoming props. OWL validates them in dev mode.static defaultPropsprovides fallback values for optional props.mount()bootstraps the root component — use only once per app.- In templates, access
propsandstatewithoutthis. In methods and getters, usethis. - Mutations to
useStateobjects automatically schedule a re-render — no explicit render calls needed. - In Odoo, components are registered in the Odoo registry rather than mounted directly.
Interview Questions
- How do you define an OWL component?
- What is the
setup()method for? - How do you register child components?
- What is
static template? - How does a component render?
Related Topics
FAQ
No — OWL only supports class-based components. There is no functional component API like React's function components or Vue's defineComponent. This is by design: OWL is purpose-built for Odoo, where the class model aligns with Odoo's existing patterns (models, controllers). Every OWL component must be a class that extends Component.
Most common cause: you forgot the xml`` tag. static template = `<div>...</div>` (without xml) assigns a plain string, not a registered template key. OWL looks up templates by key, so it cannot find a plain string. Fix: static template = xml`<div>...</div>`. Another cause: the component is not listed in the parent's static components.
No — an OWL QWeb template must have exactly one root element. If you need multiple elements at the top level, wrap them in a <div> or use the <t> invisible wrapper: <t><div/><p/></t>. The <t> tag is a structural node — it does not produce any DOM element but allows multiple children in the template.
Split when: (1) a piece of UI is reused in multiple places, (2) a template grows beyond ~30–40 lines and logical sections are clear, (3) a sub-section has its own independent state, or (4) you want to isolate re-renders — OWL only re-renders components whose state or props changed. One big monolithic component re-renders entirely on any state change; splitting lets only the affected part re-render.

