Ad – 728×90
🦉 Advanced OWL

OWL JS Sub-components – Composition & Component Trees

Real OWL applications are not built from a single monolithic component — they are composed from many smaller, focused components arranged in a tree. Understanding how to break UI into sub-components, register them correctly, communicate between them, and render them dynamically is the skill that separates beginner OWL code from production-quality Odoo modules. This lesson covers every aspect of component composition: static registration, parent-child communication patterns, dynamic t-component, and building reusable component libraries.

⏱️ 22 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 1 of 6

Registering Sub-components

Every component used in a template must be declared in the parent's static components object. OWL uses this map to look up the component class when it encounters a tag name in the template.

JavaScript – static components registration
import { Component, xml } from "@odoo/owl";
import UserAvatar  from "./UserAvatar";
import StatusBadge from "./StatusBadge";
import ActionMenu  from "./ActionMenu";

class UserCard extends Component {
  // Every component used in the template must be listed here
  static components = { UserAvatar, StatusBadge, ActionMenu };

  static template = xml`
    <div class="user-card">
      <UserAvatar src="props.user.avatarUrl" size="48"/>
      <div class="user-info">
        <strong t-esc="props.user.name"/>
        <StatusBadge status="props.user.status"/>
      </div>
      <ActionMenu items="props.actions" onSelect.bind="handleAction"/>
    </div>
  `;
}
⚠️
Forgetting static components is the most common OWL error

If UserAvatar is used in the template but not listed in static components, OWL throws "Cannot find the definition of component 'UserAvatar'". Every sub-component, at every level, must be registered by its immediate parent. Grandchildren register with their own parent — not the root.

Parent-Child Communication

OWL uses one-way data flow: data goes down via props, events go up via callback props or trigger(). This pattern keeps data flow predictable.

JavaScript – props down, callbacks up
// ── Child ──────────────────────────────────────────────────────────
class QuantityInput extends Component {
  static template = xml`
    <div class="qty-input">
      <button t-on-click="() => change(-1)" t-att-disabled="props.value <= props.min">−</button>
      <span t-esc="props.value"/>
      <button t-on-click="() => change(+1)" t-att-disabled="props.value >= props.max">+</button>
    </div>
  `;

  static props = {
    value:    { type: Number },
    min:      { type: Number, optional: true },
    max:      { type: Number, optional: true },
    onChange: { type: Function },
  };

  static defaultProps = { min: 0, max: 999 };

  change(delta) {
    const next = this.props.value + delta;
    if (next >= this.props.min && next <= this.props.max) {
      this.props.onChange(next);   // notify parent
    }
  }
}

// ── Parent ──────────────────────────────────────────────────────────
class CartItem extends Component {
  static components = { QuantityInput };

  static template = xml`
    <div class="cart-item">
      <span t-esc="props.product.name"/>
      <QuantityInput
        value="state.qty"
        min="1"
        max="99"
        onChange.bind="updateQty"
      />
      <span>$<t t-esc="(state.qty * props.product.price).toFixed(2)"/></span>
    </div>
  `;

  state = useState({ qty: 1 });

  updateQty(newQty) {
    this.state.qty = newQty;
  }
}

Dynamic Components with t-component

t-component renders a component class stored in a variable — useful for registries, plugin systems, and dynamic widget rendering (common in Odoo views).

JavaScript – t-component for dynamic rendering
import TextWidget    from "./TextWidget";
import NumberWidget  from "./NumberWidget";
import SelectWidget  from "./SelectWidget";
import DateWidget    from "./DateWidget";

// Widget registry — maps field type to component class
const WIDGET_MAP = {
  char:     TextWidget,
  integer:  NumberWidget,
  float:    NumberWidget,
  many2one: SelectWidget,
  date:     DateWidget,
};

class DynamicField extends Component {
  static components = { TextWidget, NumberWidget, SelectWidget, DateWidget };

  static template = xml`
    <div class="field-widget">
      <!-- t-component renders whichever class widgetComponent resolves to -->
      <t t-component="widgetComponent"
         value="props.field.value"
         readonly="props.readonly"
         onChange.bind="handleChange"
      />
    </div>
  `;

  static props = {
    field:    { type: Object },
    readonly: { type: Boolean, optional: true },
    onChange: { type: Function, optional: true },
  };

  get widgetComponent() {
    // Return the component class for this field type
    return WIDGET_MAP[this.props.field.type] || TextWidget;
  }

  handleChange(value) {
    this.props.onChange?.(value);
  }
}
Ad – 336×280

Composition Patterns

Container / Presentational Split

JavaScript – container + presentational
// Presentational: pure display, no data fetching
class ProductCard extends Component {
  static template = xml`
    <div class="product-card">
      <img t-att-src="props.imageUrl" t-att-alt="props.name"/>
      <h3 t-esc="props.name"/>
      <p>$<t t-esc="props.price.toFixed(2)"/></p>
      <button t-on-click="() => props.onAddToCart(props.id)">Add to Cart</button>
    </div>
  `;

  static props = {
    id: Number, name: String, imageUrl: String,
    price: Number, onAddToCart: Function,
  };
}

// Container: fetches data, passes to presentational children
class ProductListContainer extends Component {
  static components = { ProductCard };

  state = useState({ products: [], cartItems: [] });

  setup() {
    this.orm = useService("orm");
    onWillStart(() => this.loadProducts());
  }

  async loadProducts() {
    this.state.products = await this.orm.searchRead("product.product",
      [["active", "=", true]], ["id","name","list_price","image_url"], { limit: 20 });
  }

  addToCart(productId) {
    this.state.cartItems.push(productId);
  }

  static template = xml`
    <div class="product-list">
      <ProductCard
        t-foreach="state.products" t-as="p" t-key="p.id"
        id="p.id" name="p.name" imageUrl="p.image_url"
        price="p.list_price"
        onAddToCart.bind="addToCart"
      />
    </div>
  `;
}

Re-render Isolation

A major benefit of splitting into sub-components: OWL re-renders only the component whose state or props changed. A large monolithic component re-renders entirely on any change. Split components give you free performance optimization.

ScenarioMonolithic componentSplit into sub-components
User edits a text fieldEntire page re-rendersOnly the field component re-renders
One list item changesEntire list re-rendersOnly that item's component re-renders
Counter in sidebar updatesHeader, sidebar, content all re-renderOnly the counter component re-renders

📋 Summary

  • Every component used in a template must be listed in static components = { ComponentName }.
  • Data flows down via props; events go up via callback props (onChange.bind="method") or this.trigger().
  • t-component="expression" renders a component class resolved at runtime — the foundation for plugin and widget registry systems.
  • Split components into Container (data fetching, state) and Presentational (props-driven display) for clarity and reuse.
  • Sub-components isolate re-renders — only the component whose state/props changed updates, not the entire tree.

🏋️ Exercise

Build a DataTable system split into three components:

  1. TableCell: props value, type ("text"|"number"|"date"|"badge"). Renders value differently per type.
  2. TableRow: props row (object), columns (array of {key, type}), onSelect (Function). Renders a TableCell per column. Clicking the row calls onSelect(row).
  3. DataTable: props columns, rows. State: selectedId. Renders a TableRow per row, uses t-att-class to highlight the selected row. Shows "Nothing selected" or selected row data below the table.

FAQ

Can a sub-component use its parent's state directly? +

No — and this is intentional. A sub-component only receives what its parent explicitly passes as props. It cannot reach up and read parent.state.something. This isolation is what makes components reusable and testable. If a deeply nested component needs data that is far up the tree, use OWL's environment (via useEnv()) or Odoo services instead of prop drilling.

What is the difference between t-component and t-if + multiple component tags? +

t-component renders one component whose class is determined at runtime — clean for large dynamic registries. Multiple t-if/t-elif blocks with explicit component tags is more verbose but more readable for a small number of known cases (2–4 options). Use t-component when the set of possible components is open-ended, comes from a map/registry, or grows over time.