Why this chapter? I've inherited more than one addon where a child widget reached straight into its parent's state to "fix" something, and every one of them turned brittle the moment someone changed the parent. Callback props are the disciplined alternative. What is Odoo trying to solve with this? OWL removed the old
trigger/event-bus system from OWL 1 on purpose — Odoo needed child-to-parent communication that's traceable from the parent's own template, not scattered across event listeners you have to hunt down. Real-world application: ATodoItemtelling itsTodoList"delete me" or a table row telling its parent "I was clicked" are the same shape as a form field widget telling the form view "my value changed" — the pattern you'll use constantly once you start building real Odoo UI.
In Chapter 8, we established the golden rule of props: data flows one way, from parent down to child. A child component should never modify its own props directly. This creates predictable, maintainable applications where you always know where data changes originate.
But this raises an important question: what happens when a child component needs to communicate back to its parent? What if a user clicks a "Delete" button inside a TodoItem component? The child can't delete the data itself because that data belongs to the parent and was passed down as a prop.
This is where the second half of the communication loop comes in: callback props.
While data flows down through props, communication flows up through function calls. The parent passes a function to the child as a prop; when something happens inside the child (a click, a submit, an error), the child simply calls that function. The parent decides what to do with the information. This completes the data flow cycle while keeping the parent firmly in control of the application state.
Coming from OWL 1.x? The old framework had a
trigger()mechanism that dispatched custom events up the component tree, which parents listened to witht-on-event-nameon the component tag. That mechanism was removed in OWL 2.0. In OWL 2.0,t-on-works only on real DOM elements (for native events likeclickandinput), and child-to-parent communication is done exclusively with callback props. If you seethis.trigger("some-event")in old code, that's legacy OWL 1 (we'll cover migrating it in Chapter 17).
Understanding the Communication Flow
Let's visualize how this bidirectional communication works:
Parent Component
|-- State: { todos: [...], users: [...] }
|-- Data flows DOWN via props ↓
|-- Callbacks flow DOWN via props ↓
|
Child Component
|-- Receives: data props (read-only) + callback props
|-- User interaction occurs
|-- Child CALLS the callback ↑
|
Parent Component
|-- Callback runs in the parent
|-- Updates its own state
|-- New state flows DOWN as props ↓
This pattern ensures that: - Parents control the data and business logic - Children remain pure and reusable - Data flow is predictable and easy to debug - Components are loosely coupled and maintainable
Basic Callback Pattern
Let's start with a simple example. We'll create a TodoItem component that can notify its parent when it needs to be deleted.
The Child Component: TodoItem
TodoItem JavaScript (todo_item.js):
import { Component } from "@odoo/owl";
export class TodoItem extends Component {
static template = "my_module.TodoItem";
static props = {
todo: {
type: Object,
shape: {
id: Number,
text: String,
completed: Boolean,
priority: { type: String, optional: true },
dueDate: { type: String, optional: true }
}
},
// Callback props: functions the parent gives us to "call home"
onDelete: { type: Function },
onToggle: { type: Function },
onEdit: { type: Function, optional: true },
canEdit: { type: Boolean, optional: true },
canDelete: { type: Boolean, optional: true },
showPriority: { type: Boolean, optional: true }
};
static defaultProps = {
canEdit: true,
canDelete: true,
showPriority: false
};
// Event handler for delete button
onDeleteClick() {
console.log("TodoItem: Delete button clicked, calling onDelete callback");
// Call the parent's function with a payload object
this.props.onDelete({
todoId: this.props.todo.id,
todoText: this.props.todo.text
});
}
// Event handler for toggle completion
onToggleClick() {
console.log("TodoItem: Toggle clicked, calling onToggle callback");
this.props.onToggle({
todoId: this.props.todo.id,
currentStatus: this.props.todo.completed
});
}
// Event handler for edit request
onEditClick() {
console.log("TodoItem: Edit clicked, calling onEdit callback");
// Optional callbacks may be undefined — use optional chaining
this.props.onEdit?.({
todoId: this.props.todo.id,
currentText: this.props.todo.text
});
}
// Computed properties for styling
get todoClasses() {
const classes = ['todo-item'];
if (this.props.todo.completed) {
classes.push('todo-completed');
}
if (this.props.todo.priority === 'high') {
classes.push('todo-high-priority');
}
return classes.join(' ');
}
get priorityBadgeClass() {
const priorityClasses = {
low: 'badge-secondary',
medium: 'badge-warning',
high: 'badge-danger'
};
return priorityClasses[this.props.todo.priority] || 'badge-secondary';
}
}
TodoItem Template (todo_item.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.TodoItem" owl="1">
<div t-att-class="todoClasses" class="d-flex align-items-center p-3 border rounded mb-2">
<!-- Toggle Completion Checkbox -->
<div class="todo-toggle me-3">
<input type="checkbox"
class="form-check-input"
t-att-checked="props.todo.completed"
t-on-click="onToggleClick"/>
</div>
<!-- Todo Content -->
<div class="todo-content flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div class="todo-text">
<span t-att-class="props.todo.completed ? 'text-decoration-line-through text-muted' : ''">
<t t-esc="props.todo.text"/>
</span>
<!-- Priority Badge -->
<t t-if="props.showPriority and props.todo.priority">
<span t-att-class="'badge ms-2 ' + priorityBadgeClass">
<t t-esc="props.todo.priority"/>
</span>
</t>
</div>
<!-- Due Date -->
<t t-if="props.todo.dueDate">
<small class="text-muted">
Due: <t t-esc="props.todo.dueDate"/>
</small>
</t>
</div>
</div>
<!-- Action Buttons -->
<div class="todo-actions ms-3">
<t t-if="props.canEdit">
<button class="btn btn-sm btn-outline-primary me-1"
t-on-click="onEditClick">
<i class="fa fa-edit"></i>
</button>
</t>
<t t-if="props.canDelete">
<button class="btn btn-sm btn-outline-danger"
t-on-click="onDeleteClick">
<i class="fa fa-trash"></i>
</button>
</t>
</div>
</div>
</t>
</templates>
Notice that t-on-click is used on real DOM elements (the checkbox and buttons)—that's exactly what t-on- is for in OWL 2.0: listening to native browser events. The bridge to the parent is the callback prop.
The Parent Component: TodoList
Now let's create the parent component that provides those callbacks:
TodoList JavaScript (todo_list.js):
import { Component, useState } from "@odoo/owl";
import { TodoItem } from "../todo_item/todo_item";
export class TodoList extends Component {
static template = "my_module.TodoList";
static components = { TodoItem };
setup() {
this.state = useState({
todos: [
{
id: 1,
text: "Learn OWL basics",
completed: true,
priority: "medium",
dueDate: "2024-03-20"
},
{
id: 2,
text: "Build a todo app",
completed: false,
priority: "high",
dueDate: "2024-03-25"
},
{
id: 3,
text: "Master component communication",
completed: false,
priority: "low",
dueDate: "2024-03-30"
}
],
editingTodoId: null,
newTodoText: "",
showCompleted: true,
showPriority: true
});
}
// Callback: child asked us to delete a todo
onTodoDelete({ todoId, todoText }) {
console.log(`TodoList: onTodoDelete called for ID ${todoId}`);
// Show confirmation (optional)
if (confirm(`Are you sure you want to delete "${todoText}"?`)) {
// Update parent state - this will cause re-render
this.state.todos = this.state.todos.filter(todo => todo.id !== todoId);
console.log(`TodoList: Todo ${todoId} deleted successfully`);
}
}
// Callback: child asked us to toggle completion
onTodoToggle({ todoId }) {
console.log(`TodoList: onTodoToggle called for ID ${todoId}`);
// Find and update the todo
const todo = this.state.todos.find(t => t.id === todoId);
if (todo) {
todo.completed = !todo.completed;
console.log(`TodoList: Todo ${todoId} completion status changed to ${todo.completed}`);
}
}
// Callback: child asked us to start editing a todo
onTodoEdit({ todoId, currentText }) {
console.log(`TodoList: onTodoEdit called for ID ${todoId}`);
// Enter edit mode
this.state.editingTodoId = todoId;
this.state.newTodoText = currentText;
}
// Local methods for managing todos
addNewTodo() {
if (!this.state.newTodoText.trim()) return;
const newTodo = {
id: Math.max(...this.state.todos.map(t => t.id), 0) + 1,
text: this.state.newTodoText.trim(),
completed: false,
priority: "medium",
dueDate: null
};
this.state.todos.push(newTodo);
this.state.newTodoText = "";
}
saveEdit() {
if (!this.state.newTodoText.trim()) return;
const todo = this.state.todos.find(t => t.id === this.state.editingTodoId);
if (todo) {
todo.text = this.state.newTodoText.trim();
}
this.cancelEdit();
}
cancelEdit() {
this.state.editingTodoId = null;
this.state.newTodoText = "";
}
toggleShowCompleted() {
this.state.showCompleted = !this.state.showCompleted;
}
toggleShowPriority() {
this.state.showPriority = !this.state.showPriority;
}
// Computed properties
get visibleTodos() {
if (this.state.showCompleted) {
return this.state.todos;
}
return this.state.todos.filter(todo => !todo.completed);
}
get completedCount() {
return this.state.todos.filter(todo => todo.completed).length;
}
get totalCount() {
return this.state.todos.length;
}
}
TodoList Template (todo_list.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.TodoList" owl="1">
<div class="todo-list-container p-4">
<!-- Header -->
<div class="todo-header mb-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<h2>My Todo List</h2>
<p class="text-muted mb-0">
<t t-esc="completedCount"/> completed of <t t-esc="totalCount"/> todos
</p>
</div>
<!-- View Controls -->
<div class="view-controls">
<button class="btn btn-sm me-2"
t-att-class="state.showCompleted ? 'btn-primary' : 'btn-outline-primary'"
t-on-click="toggleShowCompleted">
<i class="fa fa-eye"></i>
<t t-esc="state.showCompleted ? 'Hide Completed' : 'Show All'"/>
</button>
<button class="btn btn-sm btn-outline-secondary"
t-att-class="state.showPriority ? 'active' : ''"
t-on-click="toggleShowPriority">
<i class="fa fa-flag"></i>
Priority
</button>
</div>
</div>
</div>
<!-- Add New Todo Form -->
<div class="add-todo-form mb-4 p-3 bg-light rounded">
<div class="row">
<div class="col">
<input type="text"
class="form-control"
placeholder="Add a new todo..."
t-model="state.newTodoText"
t-on-keyup.enter="addNewTodo"/>
</div>
<div class="col-auto">
<button class="btn btn-success" t-on-click="addNewTodo">
<i class="fa fa-plus"></i>
Add Todo
</button>
</div>
</div>
</div>
<!-- Edit Form -->
<t t-if="state.editingTodoId">
<div class="edit-todo-form mb-4 p-3 bg-warning bg-opacity-10 rounded">
<h5>Edit Todo</h5>
<div class="row">
<div class="col">
<input type="text"
class="form-control"
t-model="state.newTodoText"
t-on-keyup.enter="saveEdit"/>
</div>
<div class="col-auto">
<button class="btn btn-success me-2" t-on-click="saveEdit">
<i class="fa fa-check"></i>
Save
</button>
<button class="btn btn-secondary" t-on-click="cancelEdit">
<i class="fa fa-times"></i>
Cancel
</button>
</div>
</div>
</div>
</t>
<!-- Todo Items -->
<div class="todo-items">
<t t-if="visibleTodos.length > 0">
<t t-foreach="visibleTodos" t-as="todo" t-key="todo.id">
<!-- This is where we hand our callbacks to the child -->
<TodoItem
todo="todo"
canEdit="true"
canDelete="true"
showPriority="state.showPriority"
onDelete.bind="onTodoDelete"
onToggle.bind="onTodoToggle"
onEdit.bind="onTodoEdit"
/>
</t>
</t>
<!-- Empty State -->
<t t-else="">
<div class="empty-state text-center py-5">
<i class="fa fa-tasks fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No todos to show</h4>
<p class="text-muted">
<t t-if="!state.showCompleted">
All todos are completed! Toggle "Show All" to see them.
</t>
<t t-else="">
Add your first todo above to get started.
</t>
</p>
</div>
</t>
</div>
</div>
</t>
</templates>
The .bind Suffix: Small Detail, Big Deal
Look closely at how the callbacks are passed:
<TodoItem onDelete.bind="onTodoDelete"/>
The .bind suffix tells OWL to bind the function to the parent component before passing it down. Without it, when the child calls this.props.onDelete(...), the this inside onTodoDelete would be undefined and this.state would crash.
You have three equivalent options—pick one and be consistent:
<!-- Option 1 (recommended): the .bind suffix -->
<TodoItem onDelete.bind="onTodoDelete"/>
<!-- Option 2: an inline arrow function (binds `this` automatically) -->
<TodoItem onDelete="(payload) => this.onTodoDelete(payload)"/>
// Option 3: bind manually in setup()
setup() {
this.onTodoDelete = this.onTodoDelete.bind(this);
}
The .bind suffix is the idiomatic OWL 2.0 way and what you'll see throughout the Odoo codebase.
Advanced Callback Patterns
1. Native Events and Stopping Propagation
t-on- on DOM elements supports useful modifiers:
<!-- .stop calls event.stopPropagation() for you -->
<button t-on-click.stop="onDeleteClick">Delete</button>
<!-- .prevent calls event.preventDefault() -->
<form t-on-submit.prevent="onSubmit">...</form>
<!-- .enter fires only for the Enter key -->
<input t-on-keyup.enter="addNewTodo"/>
Use these to control the native DOM event; then call your callback prop as usual.
2. Conditional Callback Invocation
// Only call the callback if certain conditions are met
onDeleteClick() {
if (this.props.todo.completed) {
this.props.onDelete({ todoId: this.props.todo.id });
} else {
this.props.onConfirmDelete?.({
todoId: this.props.todo.id,
message: "This todo is not completed. Are you sure?"
});
}
}
3. Async Callbacks
Callbacks can be async, which lets the child await the parent's work (for example, to show a spinner while the parent saves):
// Child
async onSaveClick() {
this.state.saving = true;
try {
await this.props.onSave({ data: this.state.formData });
} finally {
this.state.saving = false;
}
}
4. Validation in the Parent
// Parent validates the payload before processing
onTodoUpdate({ todoId, newText }) {
// Validate the data
if (!todoId || typeof newText !== 'string' || newText.trim().length === 0) {
console.error("Invalid todo update:", { todoId, newText });
return;
}
// Proceed with update
const todo = this.state.todos.find(t => t.id === todoId);
if (todo) {
todo.text = newText.trim();
}
}
Callback Naming Conventions
Following consistent naming conventions makes your code more maintainable:
// In the child's props definition: "on" + what happened, camelCase
static props = {
onTodoCreated: { type: Function },
onUserSelected: { type: Function },
onFormSubmitted: { type: Function },
onError: { type: Function, optional: true },
};
Best Practices:
- Prefix with
on:onDelete,onSave,onUserSelected—it instantly reads as "callback" - Use camelCase: callback props are regular props, so they follow prop naming (
onTodoDeleted, noton-todo-deleted) - Be descriptive:
onUserProfileUpdatednotonUpdatewhen the context isn't obvious - Include context in payloads: pass an object (
{ todoId, todoText }) rather than loose arguments—it's self-documenting and extensible - Mark truly optional callbacks as
optional: trueand call them with?.()
Complex Communication Pattern
Let's create a more sophisticated example with a UserManager parent and multiple child components:
UserManager Parent (user_manager.js):
import { Component, useState } from "@odoo/owl";
import { UserCard } from "../user_card/user_card";
import { UserForm } from "../user_form/user_form";
import { UserFilters } from "../user_filters/user_filters";
export class UserManager extends Component {
static template = "my_module.UserManager";
static components = { UserCard, UserForm, UserFilters };
setup() {
this.state = useState({
users: [
{ id: 1, name: "Alice Johnson", role: "admin", active: true, department: "IT" },
{ id: 2, name: "Bob Smith", role: "user", active: true, department: "Sales" },
{ id: 3, name: "Carol Brown", role: "user", active: false, department: "HR" }
],
filters: {
role: "all",
department: "all",
active: true
},
editingUser: null,
showForm: false,
searchText: ""
});
}
// Callbacks for UserCard
onUserEdit({ user }) {
console.log("UserManager: Edit user requested", user);
this.state.editingUser = { ...user }; // Clone for editing
this.state.showForm = true;
}
onUserDelete({ userId, userName }) {
console.log("UserManager: Delete user requested", userId);
if (confirm(`Delete user ${userName}?`)) {
this.state.users = this.state.users.filter(u => u.id !== userId);
}
}
onUserToggleStatus({ userId }) {
console.log("UserManager: Toggle user status", userId);
const user = this.state.users.find(u => u.id === userId);
if (user) {
user.active = !user.active;
}
}
// Callbacks for UserForm
onUserSave({ user }) {
console.log("UserManager: Save user", user);
if (user.id) {
// Update existing user
const index = this.state.users.findIndex(u => u.id === user.id);
if (index !== -1) {
this.state.users[index] = user;
}
} else {
// Create new user
user.id = Math.max(...this.state.users.map(u => u.id), 0) + 1;
this.state.users.push(user);
}
this.onFormCancel();
}
onFormCancel() {
console.log("UserManager: Form cancelled");
this.state.showForm = false;
this.state.editingUser = null;
}
Three different children (UserCard, UserForm, UserFilters) each get their own dedicated callback methods above—UserManager never needs to guess which child called it, since every callback is passed explicitly to a single component in the template.
// Callbacks for UserFilters
onFiltersChanged({ filters }) {
console.log("UserManager: Filters changed", filters);
this.state.filters = { ...this.state.filters, ...filters };
}
onSearchChanged({ searchText }) {
console.log("UserManager: Search changed", searchText);
this.state.searchText = searchText;
}
// Local actions
openNewUserForm() {
this.state.editingUser = {
id: null,
name: "",
role: "user",
active: true,
department: ""
};
this.state.showForm = true;
}
Finally, filteredUsers is a plain computed getter—no props or callbacks involved—that combines the active filters and search text into the list actually shown in the template:
// Computed properties
get filteredUsers() {
let filtered = this.state.users;
// Apply role filter
if (this.state.filters.role !== "all") {
filtered = filtered.filter(u => u.role === this.state.filters.role);
}
// Apply department filter
if (this.state.filters.department !== "all") {
filtered = filtered.filter(u => u.department === this.state.filters.department);
}
// Apply active filter
if (this.state.filters.active !== null) {
filtered = filtered.filter(u => u.active === this.state.filters.active);
}
// Apply search
if (this.state.searchText) {
const search = this.state.searchText.toLowerCase();
filtered = filtered.filter(u =>
u.name.toLowerCase().includes(search) ||
u.department.toLowerCase().includes(search)
);
}
return filtered;
}
}
And in the template, each child gets exactly the callbacks it needs:
<UserFilters
filters="state.filters"
onFiltersChanged.bind="onFiltersChanged"
onSearchChanged.bind="onSearchChanged"
/>
<t t-foreach="filteredUsers" t-as="user" t-key="user.id">
<UserCard
user="user"
onEdit.bind="onUserEdit"
onDelete.bind="onUserDelete"
onToggleStatus.bind="onUserToggleStatus"
/>
</t>
<t t-if="state.showForm">
<UserForm
user="state.editingUser"
onSave.bind="onUserSave"
onCancel.bind="onFormCancel"
/>
</t>
This example demonstrates how a single parent can coordinate multiple child components through callbacks, maintaining centralized state management while allowing children to remain focused and reusable.
Data Props vs Callback Props: Decision Framework
Both travel down as props, but they serve opposite directions of communication:
Use Callback Props When:
- Child needs to notify parent of user actions
- Child needs to request parent to change data
- Child encounters an error that parent should handle
- Child completes an async operation
static props = {
onDeleted: { type: Function }, // "Something happened"
onSaveRequested: { type: Function }, // "Please do something"
onError: { type: Function }, // "Something went wrong"
};
Use Data Props When:
- Parent needs to configure child behavior
- Parent needs to pass data to child
- Parent controls child's display state
static props = {
user: { type: Object }, // Data prop
readonly: { type: Boolean }, // Configuration prop
showDetails: { type: Boolean } // State prop
};
What about components that aren't parent and child? Callbacks work through the component tree. For communication between distant components (siblings, or across the whole app), you'll use shared state and the environment bus—both covered in Chapter 15.
Debugging Callbacks
Console Logging Pattern
// In child component
onAction() {
console.log("Child: Calling onAction callback", { payload: "data" });
this.props.onAction({ payload: "data" });
}
// In parent component
onChildAction(payload) {
console.log("Parent: onChildAction received", payload);
// Handle it...
}
Common Issues and Solutions
Issue 1: "Cannot read properties of undefined (reading 'state')"
<!-- Problem: unbound function loses `this` -->
<TodoItem onDelete="onTodoDelete"/>
<!-- Solution: use the .bind suffix -->
<TodoItem onDelete.bind="onTodoDelete"/>
Issue 2: "props.onEdit is not a function"
// The parent didn't pass an optional callback.
// Declare it optional and call it safely:
static props = {
onEdit: { type: Function, optional: true },
};
onEditClick() {
this.props.onEdit?.({ todoId: this.props.todo.id });
}
Issue 3: Callback runs immediately on render
<!-- Problem: this CALLS the function during rendering -->
<TodoItem onDelete="onTodoDelete(todo.id)"/>
<!-- Solution: pass a function, don't call one -->
<TodoItem onDelete="() => this.onTodoDelete(todo.id)"/>
Performance Considerations
1. Debounce High-Frequency Callbacks
// Bad: calls the parent on every keystroke
onInputChange(event) {
this.props.onTextChanged({ value: event.target.value });
}
// Good: debounced callback. Debouncing means delaying a function call until
// a burst of triggering events (e.g. keystrokes) stops for a given period,
// so you don't fire one call per keystroke. `debounce` is available in
// @web/core/utils/timing.
setup() {
this.debouncedNotify = debounce((value) => {
this.props.onTextChanged({ value });
}, 300);
}
onInputChange(event) {
this.debouncedNotify(event.target.value);
}
2. Minimize Payload Size
// Bad: large payload with unnecessary data
this.props.onUserSelected({
fullUser: this.props.user, // Entire user object
allUsers: this.props.users // Unnecessary data
});
// Good: minimal payload
this.props.onUserSelected({
userId: this.props.user.id // Just the ID
});
3. Use Stable Function References
The .bind suffix creates the bound function once, so children don't see a "new" prop on every render. Prefer it over defining fresh arrow functions in templates when the callback doesn't need loop variables.
Common Pitfalls
Pitfall 1: Forgetting .bind
<!-- `this` inside onTodoDelete will be undefined -->
<TodoItem onDelete="onTodoDelete"/>
Always use .bind, an inline arrow function, or a manual .bind(this) in setup()—see "The .bind Suffix" above.
Pitfall 2: Calling the Callback Instead of Passing It
<!-- Problem: this CALLS onTodoDelete during rendering, not on click -->
<TodoItem onDelete="onTodoDelete(todo.id)"/>
<!-- Solution -->
<TodoItem onDelete="() => this.onTodoDelete(todo.id)"/>
Pitfall 3: Treating Optional Callbacks as Always Present
If a callback prop is optional: true, calling it directly (this.props.onEdit(...)) throws when the parent didn't pass it. Use optional chaining: this.props.onEdit?.(...).
Pitfall 4: Sending Bulky Payloads
Passing the entire component's state, or unrelated data, in a callback payload couples the child to internals it shouldn't know about. Send the minimum the parent needs (an id, a small object) instead of whole objects or arrays.
Exercises
Exercise 1: Add a Callback
Extend the TodoItem/TodoList example with a new callback prop onPriorityChange that the child calls when the user clicks a "bump priority" button. The parent should update the todo's priority in its state.
Exercise 2: Fix the Bug
Given <TodoItem onDelete="onTodoDelete"/> (no .bind, no arrow function), predict what happens when the delete button is clicked, then verify it in the browser console. Fix it using the .bind suffix.
Exercise 3: Debounced Search
Build a SearchBox component with a text input and an onSearch callback prop. Debounce the callback so it only fires 300ms after the user stops typing, using debounce from @web/core/utils/timing.
Callback props are the key to building maintainable OWL applications where components can communicate effectively while remaining loosely coupled. By mastering this pattern—data down, callbacks up—you create applications that are both powerful and predictable.
TL;DR: Children talk to parents by calling a function the parent passed down as a prop (always with .bind), never by mutating parent state or reaching for a legacy event bus.
Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch9_ex1. Installation instructions are in the repository README.
What's Next?
With data down and callbacks up, the parent-child communication loop is complete. In the next chapter, we'll explore component lifecycle hooks that let you tap into key moments in a component's life to perform initialization, cleanup, and optimization.