Skip to Content
  • Home
  • Book
  • Blog
  • About us
  • Help
  • Contact us
  •  [email protected]
  • Sign in
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contact Us
Simplify It S.R.L.
      • Home
      • Book
      • Blog
      • About us
      • Help
      • Contact us
    •  [email protected]
    • English (US) Español (BO)
    • Sign in
    • Contact Us

    Chapter 7: State and Reactivity: Making Components Dynamic

  • All Blogs
  • OWL Book
  • Chapter 7: State and Reactivity: Making Components Dynamic
  • August 25, 2026 by
    Chapter 7: State and Reactivity: Making Components Dynamic
    Grover Menacho

    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:

    1. Wraps your object in a Proxy that intercepts all property access and modifications
    2. Tracks dependencies between your template and state properties
    3. Schedules re-renders when state changes, but batches them for performance
    4. 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:

    1. Click the counter buttons: Watch the number change instantly, along with the color and message
    2. Toggle the theme: See the entire interface switch between light and dark modes immediately
    3. Edit your name: Notice how the input field appears and the header updates in real-time
    4. Add todos: Watch new items appear in the list instantly
    5. Check/uncheck todos: See the completion stats update automatically
    6. 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.


    The reactivity loop, and the one assignment that breaks it.{width=100%}

    Understanding Reactivity in Depth

    What Triggers a Re-render?

    OWL re-renders your component when:

    1. State properties change: this.state.counter++
    2. Array mutations: this.state.items.push(), this.state.items.splice()
    3. Object property updates: this.state.user.name = "New Name"
    4. 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:

    1. Batching: Multiple state changes in the same JavaScript execution cycle are batched into a single re-render
    2. Efficient Diffing: Only the parts of the DOM that actually changed are updated
    3. 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

    1. Owl DevTools: Install the Owl browser extension for component and state inspection
    2. Console Logging: Add strategic console.log statements in methods
    3. Breakpoints: Set breakpoints in state-changing methods
    4. 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

    1. Add a reset-all button. Add a button that resets counter to 0, todos to an empty array, and userName back to "Anonymous User"—all in one method—and confirm the whole UI updates at once.
    2. Trigger Pitfall 1 on purpose. In reset(), temporarily replace this.state.counter = 0 with this.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.
    3. Add a computed property. Add a new getter, oldestPendingTodo, that returns the first todo in state.todos where completed is false (or null if 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.

    in OWL Book
    Chapter 8: Props: Parent-to-Child Communication
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Useful Links
    • Home
    • About us
    • Blog
    • The OWL 2.0 book
    • Help
    • Contact us
    About us

    Simplify It S.R.L. is an Odoo implementation and development firm based in La Paz, Bolivia, working with companies across Latin America and North America. We have been building on Odoo since version 6.1: custom modules, version migrations and Bolivian localization.

    We are also the authors of the OWL 2.0 book, published free chapter by chapter on our blog.

    Connect with us
    • Contact us
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Follow us
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Powered by Odoo - Create a free website