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.
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:
<!-- 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)"/>
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.
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,
};
}
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.
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
};
}
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.
// ── 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; }
}
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.
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.
// ❌ 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 declaration | Accepts | Example |
|---|---|---|
{ type: String } | Any string | name="'Alice'" |
{ type: Number } | Any number | age="30" |
{ type: Boolean } | true or false | isActive="true" |
{ type: Object } | Any plain object | user="state.user" |
{ type: Array } | Any array | items="state.items" |
{ type: Function } | Any function | onSave.bind="save" |
{ type: [Number, String] } | Number or string | id="42" or id="'ABC'" |
true | Any value, no check | data="anything" |
{ type: X, optional: true } | X or undefined | Prop 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.
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:
RatingWidgetaccepts:value(Number, 1–5),label(String, optional, default "Rating"),readonly(Boolean, optional, default false),onChange(Function, optional).- Render 5 star buttons (★). Stars up to
valueare filled, the rest are empty. - When
readonlyis false and a star is clicked, callprops.onChange(newValue)if provided. - In a parent
Appcomponent, place twoRatingWidgets: one withreadonlytrue (static display) and one connected to state (interactive). - 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 variablevalue. static propsdeclares and type-validates incoming props in dev mode — use it for every component.static defaultPropsprovides 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
.bindsuffix to preservethis. - In templates, access
props.x. In methods and getters, accessthis.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?
Related Topics
FAQ
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.
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.
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.
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.

