Advertisement
🦉 Core Concepts

OWL JS Props – Passing Data Down the Component Tree

Props are how parent components send data to child components in OWL JS. They are read-only from the child's perspective — a child should never mutate its props directly. Instead it communicates back to the parent through callback props (functions passed down as props). This one-way data flow makes component interactions predictable and easy to trace. This lesson covers everything about props: how to pass them, how to declare and validate them with static props, how to set defaults with static defaultProps, and the patterns you will use in every real OWL application.

⏱️ 18 min read 🎯 Beginner 📅 Updated 2026 👁️ Lesson 3 of 5

What Are Props?

Props (short for "properties") are values that a parent component passes to a child component in the template. The child receives them as a frozen this.props object and can read them in its template and methods. Think of props as function arguments — they configure what the child renders without the child knowing who its caller is.

JavaScript – basic props example
import { Component, xml } from "@odoo/owl";

// Child — receives name and role as props
class UserBadge extends Component {
  static template = xml`
    <div class="user-badge">
      <strong t-esc="props.name"/>
      <span class="role" t-esc="props.role"/>
    </div>
  `;
}

// Parent — passes props in the template
class App extends Component {
  static components = { UserBadge };

  static template = xml`
    <div>
      <UserBadge name="'Alice'" role="'Admin'"/>
      <UserBadge name="'Bob'" role="'Editor'"/>
    </div>
  `;
}

Passing Props – Syntax Rules

Props are passed as attributes on the child component tag. The attribute value is always a JavaScript expression:

XML – prop passing syntax
<!-- String literals: wrap in extra single quotes -->
<UserCard name="'Alice'"/>

<!-- Numbers and booleans: no extra quotes needed -->
<UserCard age="30" isAdmin="true"/>

<!-- Variables and expressions from the parent -->
<UserCard name="state.selectedUser.name" age="state.selectedUser.age"/>

<!-- Boolean true shorthand (attribute with no value) -->
<UserCard isActive/>

<!-- Passing an object -->
<UserCard config="{ theme: 'dark', size: 'large' }"/>

<!-- Passing an array -->
<TagList tags="['js', 'owl', 'odoo']"/>

<!-- Bound method as callback prop (preserves 'this') -->
<UserCard onSave.bind="handleSave"/>

<!-- Inline arrow function -->
<UserCard onSave="(data) => handleSave(data)"/>
⚠️
The string literal trap

name="Alice" evaluates the JavaScript variable Alice — likely undefined. name="'Alice'" is a string literal. This is the single most common OWL beginner mistake. When passing a string constant, always wrap it in inner single quotes: "'my string'".

Declaring Props with static props

The static props property is an optional but strongly recommended way to declare the props a component expects, their types, and whether they are required. OWL validates against this definition in dev mode and throws a clear error if a prop is missing or the wrong type.

JavaScript – static props declaration
class UserCard extends Component {
  static props = {
    // Simple type declaration
    name:    { type: String },
    age:     { type: Number },
    isAdmin: { type: Boolean },

    // Optional prop (will not throw if absent)
    bio:     { type: String, optional: true },

    // Multiple allowed types
    id:      { type: [Number, String] },

    // Array of objects with shape
    tags: {
      type: Array,
      element: { type: Object },
    },

    // Object with specific shape
    address: {
      type: Object,
      shape: {
        street: { type: String },
        city:   { type: String },
        zip:    { type: String, optional: true },
      },
    },

    // Callback / function prop
    onSave:   { type: Function },
    onCancel: { type: Function, optional: true },

    // Any type (turns off type checking for this prop)
    data:     true,

    // Wildcard — accept any extra props (no validation)
    "*":      true,
  };
}
💡
Prop validation only runs in dev mode

OWL's prop validation is a development-time safeguard — it is disabled in production builds for performance. When calling mount(App, el, { dev: true }), validation is active. In your Odoo module, Odoo enables dev mode automatically when the browser is in debug mode.

Default Props with static defaultProps

static defaultProps provides fallback values for optional props. These are merged with the incoming props before the component renders, so a prop with a default will always have a value.

JavaScript – defaultProps
class Button extends Component {
  static template = xml`
    <button t-att-class="'btn btn--' + props.variant" t-esc="props.label"/>
  `;

  static props = {
    label:    { type: String },
    variant:  { type: String, optional: true },  // "primary" | "secondary" | "danger"
    disabled: { type: Boolean, optional: true },
    onClick:  { type: Function, optional: true },
  };

  // Default values for optional props
  static defaultProps = {
    variant:  "primary",
    disabled: false,
    onClick:  () => {},  // no-op so you can always call props.onClick() safely
  };
}
Advertisement

One-Way Data Flow

OWL enforces one-way data flow: data goes down (parent → child via props) and events go up (child → parent via callbacks or custom events). A child must never directly mutate its props — doing so would make the data flow unpredictable.

JavaScript – correct: callback prop to communicate up
// ── Child (Counter) ───────────────────────────────────────────────
class Counter extends Component {
  static template = xml`
    <div>
      <p>Count: <t t-esc="props.count"/></p>
      <button t-on-click="() => props.onIncrement()">+1</button>
      <button t-on-click="() => props.onReset()">Reset</button>
    </div>
  `;

  static props = {
    count:       { type: Number },
    onIncrement: { type: Function },
    onReset:     { type: Function },
  };
}

// ── Parent ─────────────────────────────────────────────────────────
class App extends Component {
  static components = { Counter };

  static template = xml`
    <div>
      <Counter
        count="state.count"
        onIncrement.bind="increment"
        onReset.bind="reset"
      />
    </div>
  `;

  state = useState({ count: 0 });

  increment() { this.state.count++; }
  reset()     { this.state.count = 0; }
}
ℹ️
Why .bind in the template?

onIncrement.bind="increment" passes the method with its this context already bound to the parent component. Without .bind, passing onIncrement="increment" would pass the unbound method, and this inside it would be undefined when the child calls it. Always use .bind when passing methods as callback props.

Accessing Props in the Component

Props are available as this.props in methods and getters, and as props (no this) in the template.

JavaScript – this.props in methods
class ProductCard extends Component {
  static template = xml`
    <div>
      <h3 t-esc="props.name"/>
      <p t-esc="formattedPrice"/>    <!-- uses a getter -->
      <span t-if="isOnSale" class="sale-badge">SALE</span>
    </div>
  `;

  static props = {
    name:         { type: String },
    price:        { type: Number },
    originalPrice: { type: Number, optional: true },
  };

  // Getters use this.props
  get formattedPrice() {
    return `$${this.props.price.toFixed(2)}`;
  }

  get isOnSale() {
    return this.props.originalPrice !== undefined
      && this.props.price < this.props.originalPrice;
  }
}

Prop Drilling and When to Avoid It

Prop drilling means passing a prop through multiple intermediate components just to get it to a deeply nested child. For one or two levels, this is fine. For three or more levels, it signals that you should use OWL's Environment / Services feature to share data globally, similar to React's Context API.

JavaScript – when drilling becomes a problem
// ❌ Three levels deep — drilling starts to hurt
// App → Layout → Sidebar → UserMenu → Avatar → all need 'currentUser'

// ✅ Better: put currentUser in OWL environment/service
// App provides it once, any component at any depth can get it
// via useService or useEnv (covered in Advanced OWL → Environment)

Props Type Reference

Type declarationAcceptsExample
{ type: String }Any stringname="'Alice'"
{ type: Number }Any numberage="30"
{ type: Boolean }true or falseisActive="true"
{ type: Object }Any plain objectuser="state.user"
{ type: Array }Any arrayitems="state.items"
{ type: Function }Any functiononSave.bind="save"
{ type: [Number, String] }Number or stringid="42" or id="'ABC'"
trueAny value, no checkdata="anything"
{ type: X, optional: true }X or undefinedProp may be absent

One-Way Data Flow: Props Go Down, Events Come Up

OWL (like React/Vue) enforces a direction: a parent passes data down to a child through props, and props are read-only in the child. Mutating this.props is a bug — it fights the framework and the change won't flow back:

// ❌ child must never mutate a prop
this.props.count++;

// ✅ ask the parent to change it, via a callback prop
this.props.onIncrement();

So when a child needs to change something the parent owns, the parent passes a function prop; the child calls it. Data flows down, notifications flow up. This keeps state in one place and makes bugs easy to trace.

💡
Validate props to catch mistakes early

Declare static props so OWL warns (in dev mode) when a required prop is missing or the wrong type — turning silent "undefined" render bugs into a clear console error:

static props = {
  count: Number,
  onIncrement: Function,
  label: { type: String, optional: true },
};

🏋️ Practical Exercise

Build a RatingWidget component with full prop handling:

  1. RatingWidget accepts: value (Number, 1–5), label (String, optional, default "Rating"), readonly (Boolean, optional, default false), onChange (Function, optional).
  2. Render 5 star buttons (★). Stars up to value are filled, the rest are empty.
  3. When readonly is false and a star is clicked, call props.onChange(newValue) if provided.
  4. In a parent App component, place two RatingWidgets: one with readonly true (static display) and one connected to state (interactive).
  5. Declare all props with correct types in static props.

🔥 Challenge Exercise

Pass data and a callback from a parent to a child via props, add static props validation, and set defaults. Explain that props are read-only in the child and how OWL validates them in dev mode.

📋 Summary

  • Props flow one way: parent → child. Children read props; they never mutate them.
  • Pass string literals as "'value'" (extra inner quotes). Passing "value" evaluates the variable value.
  • static props declares and type-validates incoming props in dev mode — use it for every component.
  • static defaultProps provides fallback values for optional props — always safe to read without an undefined check.
  • Pass callback functions down as props for child-to-parent communication. Always use .bind suffix to preserve this.
  • In templates, access props.x. In methods and getters, access this.props.x.
  • For deeply shared data (3+ levels), use OWL's Environment / Services instead of prop drilling.

Interview Questions

  • What are props in OWL?
  • How does a child read props?
  • Are props mutable?
  • How do you validate props?
  • How do you pass a callback to a child?

FAQ

Can I mutate props directly in the child? +

Technically you can (JavaScript does not prevent it), but you must never do so. Props are owned by the parent, and mutating them in the child causes desynchronization — the parent's state does not reflect the mutation, so the next render from the parent will overwrite your change. If the child needs to modify something, it should call a callback prop to notify the parent, which updates its state and re-passes the correct prop value. OWL freezes this.props in dev mode to catch accidental mutations early.

What happens if a required prop is not passed? +

In dev mode, OWL throws an error: "Missing props '…' for component '…'". In production mode (dev not enabled), no error is thrown, but props.missingProp is undefined. This is why declaring static props and testing in dev mode is important — it surfaces these bugs before they reach production.

Should I declare static props for every component? +

For components that other developers will use (shared components, library components), always declare static props — it is self-documenting and catches mistakes early. For purely internal leaf components used in only one place, you may skip it as a pragmatic trade-off. As a rule: the more reusable the component, the more important the prop declaration.

How do I spread all parent props onto a child? +

OWL does not have a spread-props syntax like React's <Child {...props}/>. You must pass each prop explicitly. If you find yourself wanting to spread many props through multiple levels, that is a signal to reconsider the component structure — possibly by passing the whole parent object as a single prop, or by using the environment for globally shared data.