Why this chapter? This is the mental model everything else in OWL is built on top of — props, hooks, services, even OWL 3's signals (Chapter 18) are all variations on "change the data, let the framework handle the DOM." What is Odoo trying to solve with this? Replacing manual DOM synchronization (find the element, update it, hope you didn't miss a spot) with a system where the UI is a guaranteed, automatic function of your data. Real-world application: Any dashboard, wizard, or interactive form you build professionally lives or dies on getting this right — most "the UI didn't update" support tickets trace back to a misunderstanding of exactly what's covered in this chapter.
This chapter is the most important one in the entire book. If you understand this, you understand the core philosophy of all modern UI frameworks. We are about to uncover the "magic" that makes OWL so powerful: reactivity.
Reactivity is the mechanism by which the user interface automatically updates itself when the underlying data changes. You don't tell the UI how to change; you simply change the data, and the UI reacts. This is the heart of the declarative paradigm we discussed in Chapter 1.
Think of it like a spreadsheet: when you change a cell value, all the formulas that depend on that cell automatically recalculate and update. OWL brings this same automatic updating behavior to web interfaces.
The Component's Brain: The setup() Method
So far, our components have been simple blueprints. To give them memory, behavior, and the ability to change over time, we need to introduce a special method called setup().
The setup() method is part of a component's lifecycle. It runs only once, right after the component is created but before it is rendered on the screen. It is the perfect place to:
- Initialize the component's internal data (state)
- Set up services and external connections
- Register event listeners
- Define computed values
- Configure the component's behavior
Let's start with a simple example and build up complexity:
// In hello_world.js
import { Component } from "@odoo/owl";
export class HelloWorld extends Component {
static template = "my_odoo_module.HelloWorld";
setup() {
console.log("The component is being set up!");
console.log("This runs once, before the first render");
// We'll initialize our state here
// Set up services here
// Register event listeners here
}
}
The useState Hook: Giving Your Component Memory
A component's state is an object that holds all the data the component needs to remember over its lifetime—things like form input values, loading states, user preferences, or a list of items to display.
But here's the crucial point: a plain JavaScript object isn't enough. If you change a property on a plain object, OWL has no way of knowing that it needs to re-render the UI. The interface would remain frozen, showing stale data.
To create a "reactive" state that OWL can monitor for changes, we use a special tool called the useState hook.
What is a Hook?
A "hook" in OWL is a function that lets you "hook into" OWL's powerful features like state management, lifecycle events, or services. Hooks can only be called inside the setup() method and they give your component superpowers.
Using useState
Here's how you create reactive state:
// In hello_world.js
import { Component, useState } from "@odoo/owl"; // 1. Import useState
export class HelloWorld extends Component {
static template = "my_odoo_module.HelloWorld";
setup() {
// 2. Create a reactive state object
this.state = useState({
counter: 0,
userName: "Guest",
isVisible: true,
items: []
});
console.log("Initial state:", this.state);
}
}
Now, this.state is not just a regular object—it's a reactive proxy. OWL is actively monitoring it. Any change made to this.state.counter, this.state.userName, or any other property will automatically trigger a re-render of the component.
The Magic Behind useState
When you call useState, OWL:
- Wraps your object in a Proxy that intercepts all property access and modifications
- Tracks dependencies between your template and state properties
- Schedules re-renders when state changes, but batches them for performance
- Updates only what changed rather than re-rendering the entire component
This is why OWL apps are so fast and responsive!
The Magic Moment: Modifying State from User Events
Let's see reactivity in action. We'll create a component with multiple interactive elements to demonstrate how state changes drive UI updates.
1. Enhanced JavaScript (hello_world.js)
setup() seeds one reactive object with everything the template will need: a counter, an editable user name, a theme flag, and a todo list.
import { Component, useState } from "@odoo/owl";
export class HelloWorld extends Component {
static template = "my_odoo_module.HelloWorld";
setup() {
this.state = useState({
counter: 0,
userName: "Anonymous User",
theme: "light",
todos: [
{ id: 1, text: "Learn OWL reactivity", completed: false },
{ id: 2, text: "Build awesome components", completed: false }
],
isEditing: false,
newTodoText: ""
});
console.log("Component initialized with state:", this.state);
}
// ... more methods below, still inside the same class
These methods only ever mutate this.state—never the DOM directly. The counter methods increment, decrement, or reset a single number; the theme method flips a string between two values; the user-name methods toggle an isEditing flag and read from an input event:
// Counter methods
increment() {
this.state.counter++;
console.log("Counter incremented to:", this.state.counter);
}
decrement() {
this.state.counter--;
console.log("Counter decremented to:", this.state.counter);
}
reset() {
this.state.counter = 0;
console.log("Counter reset to:", this.state.counter);
}
// Theme methods
toggleTheme() {
this.state.theme = this.state.theme === "light" ? "dark" : "light";
console.log("Theme changed to:", this.state.theme);
}
// User name methods
startEditing() {
this.state.isEditing = true;
}
saveUserName() {
if (this.state.userName.trim()) {
this.state.isEditing = false;
console.log("User name saved:", this.state.userName);
}
}
onUserNameInput(event) {
this.state.userName = event.target.value;
}
The todo methods use array methods (push, find, splice) directly on the reactive todos array—OWL's reactivity wraps arrays too, so mutating them in place still triggers a re-render. The three get accessors at the end are computed properties: they aren't stored in state, they're recalculated from it every time the template reads them:
// Todo methods
addTodo() {
if (this.state.newTodoText.trim()) {
const newTodo = {
id: Math.max(0, ...this.state.todos.map(t => t.id)) + 1,
text: this.state.newTodoText.trim(),
completed: false
};
this.state.todos.push(newTodo);
this.state.newTodoText = "";
console.log("Added new todo:", newTodo);
}
}
toggleTodo(id) {
const todo = this.state.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
console.log("Toggled todo:", todo);
}
}
removeTodo(id) {
const index = this.state.todos.findIndex(t => t.id === id);
if (index !== -1) {
const removed = this.state.todos.splice(index, 1)[0];
console.log("Removed todo:", removed);
}
}
onNewTodoInput(event) {
this.state.newTodoText = event.target.value;
}
// Computed properties (calculated from state)
get completedCount() {
return this.state.todos.filter(todo => todo.completed).length;
}
get remainingCount() {
return this.state.todos.length - this.completedCount;
}
get allCompleted() {
return this.state.todos.length > 0 && this.completedCount === this.state.todos.length;
}
}
2. Comprehensive Template (hello_world.xml)
We'll look at this template in three pieces. First, the header: it swaps between a read-only greeting and an editable input based on state.isEditing, plus a theme-toggle button:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_odoo_module.HelloWorld" owl="1">
<div class="hello-world-app p-4"
t-att-class="{ 'bg-dark text-white': state.theme === 'dark' }">
<!-- Header with User Name -->
<div class="header-section mb-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<t t-if="!state.isEditing">
<h2 class="mb-1">
Hello, <t t-esc="state.userName"/>!
<button class="btn btn-sm btn-outline-secondary ms-2"
t-on-click="startEditing">
<i class="fa fa-edit"></i>
</button>
</h2>
</t>
<t t-else="">
<div class="input-group mb-2">
<input type="text"
class="form-control"
t-att-value="state.userName"
t-on-input="onUserNameInput"
placeholder="Enter your name"/>
<button class="btn btn-success" t-on-click="saveUserName">
<i class="fa fa-check"></i> Save
</button>
</div>
</t>
</div>
<!-- Theme Toggle -->
<button class="btn"
t-att-class="state.theme === 'dark' ? 'btn-light' : 'btn-dark'"
t-on-click="toggleTheme">
<i t-att-class="state.theme === 'dark' ? 'fa fa-sun' : 'fa fa-moon'"></i>
<t t-esc="state.theme === 'dark' ? 'Light' : 'Dark'"/> Theme
</button>
</div>
</div>
Next, the counter (same template, continued). Notice the three-way t-att-class on the number itself, and the t-if/t-elif/t-else chain that picks a different message depending on whether the count is zero, positive, or negative:
<!-- Counter Section -->
<div class="counter-section mb-4">
<div class="card" t-att-class="{ 'bg-secondary': state.theme === 'dark' }">
<div class="card-body text-center">
<h3 class="display-4 mb-3">
<span t-att-class="{
'text-success': state.counter > 0,
'text-danger': state.counter < 0,
'text-muted': state.counter === 0
}">
<t t-esc="state.counter"/>
</span>
</h3>
<div class="btn-group" role="group">
<button class="btn btn-danger" t-on-click="decrement">
<i class="fa fa-minus"></i> Decrease
</button>
<button class="btn btn-secondary"
t-att-disabled="state.counter === 0"
t-on-click="reset">
Reset
</button>
<button class="btn btn-success" t-on-click="increment">
<i class="fa fa-plus"></i> Increase
</button>
</div>
<!-- Dynamic messages based on counter value -->
<div class="mt-3">
<t t-if="state.counter === 0">
<p class="text-muted mb-0">Start counting!</p>
</t>
<t t-elif="state.counter > 0">
<p class="text-success mb-0">
<t t-if="state.counter === 1">You've clicked once!</t>
<t t-else="">You've clicked <t t-esc="state.counter"/> times!</t>
</p>
</t>
<t t-else="">
<p class="text-danger mb-0">Going negative: <t t-esc="Math.abs(state.counter)"/> below zero</p>
</t>
</div>
</div>
</div>
</div>
Finally, the todo list and a small debug panel, then we close every tag opened above. The list combines t-foreach with a nested t-if (for the "all completed" banner) and a t-else for the empty state:
<!-- Todo List Section -->
<div class="todo-section">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>Todo List</h3>
<div class="todo-stats">
<span class="badge bg-primary me-2">
<t t-esc="remainingCount"/> remaining
</span>
<span class="badge bg-success">
<t t-esc="completedCount"/> completed
</span>
</div>
</div>
<!-- Add New Todo -->
<div class="add-todo mb-3">
<div class="input-group">
<input type="text"
class="form-control"
placeholder="What needs to be done?"
t-att-value="state.newTodoText"
t-on-input="onNewTodoInput"
t-on-keyup.enter="addTodo"/>
<button class="btn btn-primary"
t-att-disabled="!state.newTodoText.trim()"
t-on-click="addTodo">
<i class="fa fa-plus"></i> Add Todo
</button>
</div>
</div>
<!-- Todo List -->
<t t-if="state.todos.length > 0">
<!-- Success message when all completed -->
<t t-if="allCompleted">
<div class="alert alert-success">
<i class="fa fa-trophy"></i>
Congratulations! You've completed all your todos!
</div>
</t>
<div class="todo-list">
<t t-foreach="state.todos" t-as="todo" t-key="todo.id">
<div class="todo-item card mb-2"
t-att-class="{ 'opacity-75': todo.completed }">
<div class="card-body d-flex align-items-center">
<div class="form-check me-3">
<input class="form-check-input"
type="checkbox"
t-att-checked="todo.completed"
t-on-change="() => this.toggleTodo(todo.id)"/>
</div>
<div class="todo-text flex-grow-1">
<span t-att-class="{ 'text-decoration-line-through text-muted': todo.completed }">
<t t-esc="todo.text"/>
</span>
</div>
<button class="btn btn-sm btn-outline-danger"
t-on-click="() => this.removeTodo(todo.id)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</t>
</div>
</t>
<t t-else="">
<div class="empty-state text-center py-5">
<i class="fa fa-clipboard-list fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No todos yet</h4>
<p class="text-muted">Add your first todo above to get started!</p>
</div>
</t>
</div>
<!-- Debug Information -->
<div class="debug-info mt-4 p-3 border rounded" t-att-class="{ 'border-light': state.theme === 'dark' }">
<h5>Debug Information</h5>
<small class="text-muted">
<strong>State Summary:</strong><br/>
Counter: <t t-esc="state.counter"/>,
Theme: <t t-esc="state.theme"/>,
Todos: <t t-esc="state.todos.length"/>,
Editing: <t t-esc="state.isEditing"/>
</small>
</div>
</div>
</t>
</templates>
3. Test the Magic!
Now, upgrade your module and view the component. Here's what you'll experience:
- Click the counter buttons: Watch the number change instantly, along with the color and message
- Toggle the theme: See the entire interface switch between light and dark modes immediately
- Edit your name: Notice how the input field appears and the header updates in real-time
- Add todos: Watch new items appear in the list instantly
- Check/uncheck todos: See the completion stats update automatically
- Delete todos: Watch items disappear and stats recalculate
The remarkable thing: We never wrote a single line of code to update the DOM directly. We only changed the data, and OWL handled all the visual updates automatically.
{width=100%}
Understanding Reactivity in Depth
What Triggers a Re-render?
OWL re-renders your component when:
- State properties change:
this.state.counter++ - Array mutations:
this.state.items.push(),this.state.items.splice() - Object property updates:
this.state.user.name = "New Name" - Nested property changes:
this.state.settings.theme = "dark"
What Doesn't Trigger a Re-render?
// This won't work - replacing the state object
this.state = { counter: 5 }; // OWL loses the reactive connection
// This won't work - modifying non-reactive objects
const plainObject = { count: 0 };
plainObject.count++; // Not reactive
// These work - modifying the reactive state
this.state.counter = 5; // Direct assignment
this.state.items.push(newItem); // Array mutation
delete this.state.temporaryData; // Property deletion
Performance and Batching
OWL is smart about performance. It doesn't re-render after every single state change. Instead:
- Batching: Multiple state changes in the same JavaScript execution cycle are batched into a single re-render
- Efficient Diffing: Only the parts of the DOM that actually changed are updated
- Dependency Tracking: OWL knows exactly which template parts depend on which state properties
// This will only cause ONE re-render, not three
updateMultipleValues() {
this.state.counter++; // Change 1
this.state.userName = "Bob"; // Change 2
this.state.theme = "dark"; // Change 3
// OWL batches these and re-renders once
}
Advanced State Patterns
Computed Properties with Getters
Create derived values that automatically update when their dependencies change:
setup() {
this.state = useState({
firstName: "John",
lastName: "Doe",
items: [/* ... */]
});
}
// These getters automatically "recompute" when state changes
get fullName() {
return `${this.state.firstName} ${this.state.lastName}`;
}
get expensiveItems() {
return this.state.items.filter(item => item.price > 100);
}
get totalValue() {
return this.expensiveItems.reduce((sum, item) => sum + item.price, 0);
}
Use in template:
<h3>Welcome, <t t-esc="fullName"/>!</h3>
<p>You have <t t-esc="expensiveItems.length"/> expensive items worth $<t t-esc="totalValue"/></p>
Complex State Structures
setup() {
this.state = useState({
user: {
profile: {
name: "Alice",
avatar: "/path/to/avatar.png",
preferences: {
theme: "dark",
language: "en"
}
},
permissions: ["read", "write"]
},
application: {
currentView: "dashboard",
loading: false,
errors: []
},
data: {
items: [],
filters: {
category: "all",
sortBy: "name",
ascending: true
}
}
});
}
// All of these trigger re-renders:
updateUserName(newName) {
this.state.user.profile.name = newName; // Nested property change
}
changeTheme(theme) {
this.state.user.profile.preferences.theme = theme; // Deep nested change
}
addError(error) {
this.state.application.errors.push(error); // Array mutation
}
State Validation and Constraints
setup() {
this.state = useState({
counter: 0,
email: "",
isValid: true
});
}
setEmail(email) {
this.state.email = email;
this.state.isValid = this.validateEmail(email);
}
validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
setCounter(value) {
// Constrain counter between 0 and 100
this.state.counter = Math.max(0, Math.min(100, value));
}
Escaping Reactivity: markRaw and toRaw
useState wraps whatever object you give it in a reactive proxy, so OWL can notice when a property is read (to track it) and when it's written (to trigger a re-render). That's usually exactly what you want—but not always.
Sometimes an object shouldn't become reactive at all: a large dataset you'll never mutate in place, or an instance from a third-party library (a charting library, a map widget) that manages its own internal state and doesn't expect to be wrapped in a proxy. Wrapping it anyway wastes performance tracking keys nobody reads, and can even break libraries that do identity checks (===) internally, since a proxy is never === to the object it wraps.
markRaw(object) tells OWL's reactivity system to leave an object alone. If it's ever nested inside a reactive object, it's stored and returned as-is—untouched, unobserved:
import { Component, useState, markRaw } from "@odoo/owl";
setup() {
// chartInstance manages its own internal state; we never want OWL
// watching its properties or wrapping it in a proxy.
const chartInstance = markRaw(new ThirdPartyChart());
this.state = useState({
counter: 0,
chart: chartInstance,
});
// Mutating chart's own properties won't trigger a re-render—as intended.
this.state.chart.zoomLevel = 2;
}
toRaw(reactiveObject) does the opposite job: given a reactive object (or a piece of one), it returns the original, non-proxied object underneath. This is handy for identity comparisons (obj === toRaw(state.obj) can be true even when obj === state.obj is false, since state.obj is a proxy) and for debugging—logging a raw object is easier to read than logging a proxy.
const original = { name: "Alice" };
const state = useState({ user: original });
console.log(original === state.user); // false — state.user is a proxy
console.log(original === toRaw(state.user)); // true
You won't reach for either of these often, but they're worth knowing about the day you integrate a chart, a map, or any other library that owns its own state.
Common Pitfalls and Solutions
Pitfall 1: Losing Reactivity
// Wrong - This breaks reactivity
someMethod() {
this.state = { items: [1, 2, 3] }; // Completely replaces the reactive object
}
// Correct - Modify properties, don't replace the object
someMethod() {
this.state.items = [1, 2, 3]; // Modifies the reactive state
}
Pitfall 2: Forgetting useState
// Wrong - plain object: changes are invisible to OWL, the UI never updates
setup() {
this.state = { counter: 0 };
}
// Correct - reactive object: changes trigger re-renders
setup() {
this.state = useState({ counter: 0 });
}
Pitfall 3: Async State Updates
// Race conditions possible
async fetchData() {
this.state.loading = true;
const data = await this.service.getData();
this.state.data = data;
this.state.loading = false; // What if component was destroyed meanwhile?
}
// Better approach: OWL's status() helper tells you if the component is still alive
import { status } from "@odoo/owl";
async fetchData() {
this.state.loading = true;
try {
const data = await this.service.getData();
if (status(this) === "destroyed") return; // Component is gone, stop here
this.state.data = data;
} finally {
if (status(this) !== "destroyed") {
this.state.loading = false;
}
}
}
State vs Props: When to Use Which
Use useState when:
- Data belongs to this component
- Data changes based on user interactions in this component
- Data is temporary or session-specific
- You need to modify the data
Use Props when: - Data comes from a parent component - Data is configuration or settings - Multiple components need to display the same data - The data is read-only for this component
// State: Component-owned data
this.state = useState({
inputValue: "", // User typing in this component
isLoading: false, // This component's loading state
showModal: false // This component's UI state
});
// Props: Parent-provided data
static props = {
userId: { type: Number }, // Configuration from parent
readonly: { type: Boolean }, // Behavior setting
initialData: { type: Array } // Data to display
};
Debugging Reactive State
Browser DevTools Tips
- Owl DevTools: Install the Owl browser extension for component and state inspection
- Console Logging: Add strategic
console.logstatements in methods - Breakpoints: Set breakpoints in state-changing methods
- State Snapshots: Log entire state at key moments
// Debugging helper method
debugState(action) {
console.group(`State Change: ${action}`);
console.log("Current state:", JSON.parse(JSON.stringify(this.state)));
console.log("Component:", this);
console.groupEnd();
}
// Use in methods
increment() {
this.state.counter++;
this.debugState("increment");
}
Common Debug Scenarios
<!-- Add temporary debugging to templates -->
<div class="debug-panel" style="position: fixed; top: 10px; right: 10px; background: yellow; padding: 10px;">
<pre t-esc="JSON.stringify(state, null, 2)"/>
</div>
// Method to reset state for testing
resetToInitialState() {
Object.assign(this.state, {
counter: 0,
userName: "Guest",
todos: [],
// ... other initial values
});
}
The Declarative Mindset
This chapter introduced you to the fundamental shift in thinking that modern UI frameworks require:
Instead of thinking: "When the user clicks this button, I need to find the counter element in the DOM and update its text"
Think: "When the user clicks this button, I need to update the counter value in my state, and the UI will automatically reflect this change"
This declarative approach makes your code:
- More predictable: The UI is always a function of your state
- Easier to debug: You can inspect state to understand the UI
- More maintainable: No manual DOM manipulation to keep track of
- More testable: You can test state changes without involving the DOM
TL;DR: Wrap data in useState() to make it reactive; mutate its properties directly (state.counter++) rather than reassigning the whole object, and the template re-renders automatically — you change data, OWL updates the DOM.
Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch7_ex1. Installation instructions are in the repository README.
Exercises
- Add a reset-all button. Add a button that resets
counterto0,todosto an empty array, anduserNameback to"Anonymous User"—all in one method—and confirm the whole UI updates at once. - Trigger Pitfall 1 on purpose. In
reset(), temporarily replacethis.state.counter = 0withthis.state = { counter: 0 }(as shown in Pitfall 1 above). Click the button and observe that the UI silently stops updating. Undo the change and confirm it works again. - Add a computed property. Add a new getter,
oldestPendingTodo, that returns the first todo instate.todoswherecompletedisfalse(ornullif there isn't one), and display it in the template.
What's Next?
Reactive state is great for data a component owns, but real applications are made of components talking to each other. Chapter 8 covers props, the mechanism for passing data from a parent component down to its children.