Advertisement
🦉 Core Concepts

OWL JS Components – The Class-Based Component Model

Everything in an OWL JS application is a component. A component is a JavaScript class that extends Component, declares a QWeb template, and optionally owns reactive state, lifecycle hooks, and child components. This lesson covers the complete anatomy of an OWL component — from the minimal skeleton to the full set of static properties — so you understand exactly what each part does and when to use it.

⏱️ 20 min read 🎯 Beginner 📅 Updated 2026 👁️ Lesson 1 of 5

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.

JavaScript – minimal component
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.

💡
Why 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:

JavaScript – full anatomy
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
templatetemplate key (from xml``)YesThe QWeb template to render for this component
components{ [name]: ComponentClass }Only if template uses sub-componentsRegisters child components so OWL can instantiate them by name in the template
propsprops definition objectNo (recommended)Declares and type-checks props passed to this component. OWL throws in dev mode if a prop violates the spec.
defaultPropsplain objectNoDefault 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.

JavaScript – mounting the root component
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() vs manual instantiation

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.

JavaScript – parent/child component tree
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);
  }
}
⚠️
Register sub-components in 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.

Advertisement

Passing Props

Props are passed in the template using attribute-like syntax. There are three syntaxes depending on the value type:

XML – three ways to pass props
<!-- 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" />
💡
String vs expression — the common pitfall

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):

XML – template scope
<!-- 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>
JavaScript – in methods you use 'this'
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.

TriggerWhat happens
Mutation on a useState objectOWL schedules a micro-task re-render for the owning component
Parent re-renders and passes new propsChild re-renders automatically if props changed
Child receives same prop valuesChild does NOT re-render (shallow prop comparison)
Manual this.render()Forces a re-render — almost never needed

Naming Conventions

ThingConventionExample
Component classPascalCaseUserCard, TodoList, InvoiceLine
Template tag in parent templatePascalCase (same as class name)<UserCard .../>
Component filePascalCase or kebab-caseUserCard.js or user-card.js
PropscamelCaseuserName, onDelete, isActive
Event handler methodsverb + nounhandleClick, toggleMenu, submitForm
State fieldscamelCaseisLoading, 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.

JavaScript – Odoo module component registration
/** @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
}
PartPurpose
static templatethe XML markup (inline via xml`` or by name)
static propsdeclares 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:

  1. Create a Badge component that accepts a label (String) prop and a color (String, optional, default "blue") prop. Render <span class="badge">{label}</span> with an inline style for the color.
  2. Create a UserCard component that accepts name, role, and tags (Array) props. Use t-foreach to render a <Badge> for each tag.
  3. Create a UserList root component with a hard-coded array of 3 users in useState. 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 Component with a static template = xml`...`.
  • static components registers child components used in the template — required for every sub-component.
  • static props declares and type-checks incoming props. OWL validates them in dev mode.
  • static defaultProps provides fallback values for optional props.
  • mount() bootstraps the root component — use only once per app.
  • In templates, access props and state without this. In methods and getters, use this.
  • Mutations to useState objects 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?

FAQ

Can I use functional components in OWL? +

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.

Why do I get "Template … not found" even though I wrote the template? +

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.

Can a component have multiple root elements in its template? +

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.

When should I split one component into multiple? +

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.