Ir al contenido
  • Inicio
  • Libro
  • Blog
  • Sobre nosotros
  • Ayuda
  • Contáctanos
  •  [email protected]
  • Inicia sesión
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contáctanos
Simplify It S.R.L.
      • Inicio
      • Libro
      • Blog
      • Sobre nosotros
      • Ayuda
      • Contáctanos
    •  [email protected]
    • English (US) Español (BO)
    • Inicia sesión
    • Contáctanos

    Capítulo 7: Estado y Reactividad: Haciendo Componentes Dinámicos

  • Todos los blogs
  • Libro OWL
  • Capítulo 7: Estado y Reactividad: Haciendo Componentes Dinámicos
  • 25 de agosto de 2026 por
    Capítulo 7: Estado y Reactividad: Haciendo Componentes Dinámicos
    Grover Menacho

    ¿Por qué este capítulo? Este es el modelo mental sobre el que se construye todo lo demás en OWL — props, hooks, servicios, incluso las signals de OWL 3 (Capítulo 18) son variaciones de "cambia el dato, deja que el framework maneje el DOM". ¿Qué trata de resolver Odoo con esto? Reemplazar la sincronización manual del DOM (encontrar el elemento, actualizarlo, esperar no haberte olvidado de algo) por un sistema donde la UI es una función garantizada y automática de tus datos. Aplicación en la vida real: Cualquier dashboard, wizard o formulario interactivo que construyas profesionalmente depende de entender bien esto — la mayoría de los tickets de soporte de "la UI no se actualizó" se rastrean hasta un malentendido de exactamente lo que cubre este capítulo.

    Este capítulo es el más importante de todo el libro. Si entiendes esto, entiendes la filosofía central de todos los frameworks de UI modernos. Estamos a punto de descubrir la "magia" que hace a OWL tan poderoso: la reactividad.

    La reactividad es el mecanismo por el cual la interfaz de usuario se actualiza automáticamente cuando los datos subyacentes cambian. No le dices a la UI cómo cambiar; simplemente cambias los datos, y la UI reacciona. Este es el corazón del paradigma declarativo que discutimos en el Capítulo 1.

    Piensa en ello como una hoja de cálculo: cuando cambias el valor de una celda, todas las fórmulas que dependen de esa celda automáticamente se recalculan y actualizan. OWL trae este mismo comportamiento de actualización automática a las interfaces web.


    El Cerebro del Componente: El Método setup()

    Hasta ahora, nuestros componentes han sido simples planos. Para darles memoria, comportamiento y la capacidad de cambiar con el tiempo, necesitamos introducir un método especial llamado setup().

    El método setup() es parte del ciclo de vida de un componente. Se ejecuta solo una vez, justo después de que el componente es creado pero antes de que sea renderizado en la pantalla. Es el lugar perfecto para:

    • Inicializar los datos internos del componente (estado)
    • Configurar servicios y conexiones externas
    • Registrar event listeners
    • Definir valores computados
    • Configurar el comportamiento del componente

    Comencemos con un ejemplo simple y construyamos complejidad:

    // En hello_world.js
    import { Component } from "@odoo/owl";
    
    export class HelloWorld extends Component {
      static template = "my_odoo_module.HelloWorld";
    
      setup() {
        console.log("¡El componente se está configurando!");
        console.log("Esto se ejecuta una vez, antes del primer render");
    
        // Inicializaremos nuestro estado aquí
        // Configuraremos servicios aquí
        // Registraremos event listeners aquí
      }
    }
    

    El Hook useState: Dándole Memoria a tu Componente

    El estado de un componente es un objeto que contiene todos los datos que el componente necesita recordar durante su vida—cosas como valores de entrada de formularios, estados de carga, preferencias del usuario, o una lista de elementos a mostrar.

    Pero aquí está el punto crucial: un objeto JavaScript simple no es suficiente. Si cambias una propiedad en un objeto simple, OWL no tiene forma de saber que necesita re-renderizar la UI. La interfaz permanecería congelada, mostrando datos obsoletos.

    Para crear un estado "reactivo" que OWL pueda monitorear para cambios, usamos una herramienta especial llamada el hook useState.

    ¿Qué es un Hook?

    Un "hook" en OWL es una función que te permite "engancharte" a las características poderosas de OWL como la gestión de estado, eventos del ciclo de vida, o servicios. Los hooks solo pueden ser llamados dentro del método setup() y le dan superpoderes a tu componente.

    Usando useState

    Así es como creas estado reactivo:

    // En hello_world.js
    import { Component, useState } from "@odoo/owl"; // 1. Importar useState
    
    export class HelloWorld extends Component {
      static template = "my_odoo_module.HelloWorld";
    
      setup() {
        // 2. Crear un objeto de estado reactivo
        this.state = useState({
          counter: 0,
          userName: "Invitado",
          isVisible: true,
          items: []
        });
    
        console.log("Estado inicial:", this.state);
      }
    }
    

    Ahora, this.state no es solo un objeto regular—es un proxy reactivo. OWL lo está monitoreando activamente. Cualquier cambio hecho a this.state.counter, this.state.userName, o cualquier otra propiedad automáticamente desencadenará un re-render del componente.

    La Magia Detrás de useState

    Cuando llamas a useState, OWL:

    1. Envuelve tu objeto en un Proxy que intercepta todo acceso y modificación de propiedades
    2. Rastrea dependencias entre tu plantilla y las propiedades del estado
    3. Programa re-renders cuando el estado cambia, pero los agrupa por rendimiento
    4. Actualiza solo lo que cambió en lugar de re-renderizar todo el componente

    ¡Por eso las aplicaciones OWL son tan rápidas y responsivas!


    El Momento Mágico: Modificando Estado desde Eventos de Usuario

    Veamos la reactividad en acción. Crearemos un componente con múltiples elementos interactivos para demostrar cómo los cambios de estado impulsan las actualizaciones de la UI.

    1. JavaScript Mejorado (hello_world.js)

    setup() inicializa un único objeto reactivo con todo lo que la plantilla necesitará: un contador, un nombre de usuario editable, un indicador de tema y una lista de tareas.

    import { Component, useState } from "@odoo/owl";
    
    export class HelloWorld extends Component {
      static template = "my_odoo_module.HelloWorld";
    
      setup() {
        this.state = useState({
          counter: 0,
          userName: "Usuario Anónimo",
          theme: "light",
          todos: [
            { id: 1, text: "Aprender reactividad OWL", completed: false },
            { id: 2, text: "Construir componentes increíbles", completed: false }
          ],
          isEditing: false,
          newTodoText: ""
        });
    
        console.log("Componente inicializado con estado:", this.state);
      }
    
      // ... más métodos abajo, todavía dentro de la misma clase
    

    Estos métodos solo mutan this.state—nunca el DOM directamente. Los métodos del contador incrementan, decrementan o reinician un solo número; el método de tema alterna un string entre dos valores; los métodos de nombre de usuario alternan un flag isEditing y leen de un evento de input:

      // Métodos del contador
      increment() {
        this.state.counter++;
        console.log("Contador incrementado a:", this.state.counter);
      }
    
      decrement() {
        this.state.counter--;
        console.log("Contador decrementado a:", this.state.counter);
      }
    
      reset() {
        this.state.counter = 0;
        console.log("Contador reiniciado a:", this.state.counter);
      }
    
      // Métodos de tema
      toggleTheme() {
        this.state.theme = this.state.theme === "light" ? "dark" : "light";
        console.log("Tema cambiado a:", this.state.theme);
      }
    
      // Métodos de nombre de usuario
      startEditing() {
        this.state.isEditing = true;
      }
    
      saveUserName() {
        if (this.state.userName.trim()) {
          this.state.isEditing = false;
          console.log("Nombre de usuario guardado:", this.state.userName);
        }
      }
    
      onUserNameInput(event) {
        this.state.userName = event.target.value;
      }
    

    Los métodos de todos usan métodos de array (push, find, splice) directamente sobre el array reactivo todos—la reactividad de OWL también envuelve arrays, así que mutarlos en el lugar sigue disparando un re-render. Los tres get accessors al final son propiedades computadas: no se guardan en el estado, se recalculan desde él cada vez que la plantilla los lee:

      // Métodos de todos
      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("Nuevo todo añadido:", newTodo);
        }
      }
    
      toggleTodo(id) {
        const todo = this.state.todos.find(t => t.id === id);
        if (todo) {
          todo.completed = !todo.completed;
          console.log("Todo alternado:", 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("Todo eliminado:", removed);
        }
      }
    
      onNewTodoInput(event) {
        this.state.newTodoText = event.target.value;
      }
    
      // Propiedades computadas (calculadas desde el estado)
      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. Plantilla Integral (hello_world.xml)

    Veremos esta plantilla en tres partes. Primero, el encabezado: alterna entre un saludo de solo lectura y un input editable según state.isEditing, más un botón para alternar el tema:

    <?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' }">
    
                <!-- Encabezado con Nombre de Usuario -->
                <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">
                                    ¡Hola, <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="Ingresa tu nombre"/>
                                    <button class="btn btn-success" t-on-click="saveUserName">
                                        <i class="fa fa-check"></i> Guardar
                                    </button>
                                </div>
                            </t>
                        </div>
    
                        <!-- Alternar Tema -->
                        <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>
                            Tema <t t-esc="state.theme === 'dark' ? 'Claro' : 'Oscuro'"/>
                        </button>
                    </div>
                </div>
    

    Ahora, el contador (misma plantilla, continuación). Nota el t-att-class de tres vías sobre el número, y la cadena t-if/t-elif/t-else que elige un mensaje distinto según si el contador es cero, positivo o negativo:

                <!-- Sección del Contador -->
                <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> Decrementar
                                </button>
                                <button class="btn btn-secondary" 
                                        t-att-disabled="state.counter === 0"
                                        t-on-click="reset">
                                    Reiniciar
                                </button>
                                <button class="btn btn-success" t-on-click="increment">
                                    <i class="fa fa-plus"></i> Incrementar
                                </button>
                            </div>
    
                            <!-- Mensajes dinámicos basados en el valor del contador -->
                            <div class="mt-3">
                                <t t-if="state.counter === 0">
                                    <p class="text-muted mb-0">¡Comienza a contar!</p>
                                </t>
                                <t t-elif="state.counter > 0">
                                    <p class="text-success mb-0">
                                        <t t-if="state.counter === 1">¡Has hecho clic una vez!</t>
                                        <t t-else="">¡Has hecho clic <t t-esc="state.counter"/> veces!</t>
                                    </p>
                                </t>
                                <t t-else="">
                                    <p class="text-danger mb-0">Yendo negativo: <t t-esc="Math.abs(state.counter)"/> bajo cero</p>
                                </t>
                            </div>
                        </div>
                    </div>
                </div>
    

    Finalmente, la lista de tareas y un pequeño panel de depuración, y cerramos cada etiqueta abierta arriba. La lista combina t-foreach con un t-if anidado (para el banner de "todo completado") y un t-else para el estado vacío:

                <!-- Sección de Lista de Todos -->
                <div class="todo-section">
                    <div class="d-flex justify-content-between align-items-center mb-3">
                        <h3>Lista de Tareas</h3>
                        <div class="todo-stats">
                            <span class="badge bg-primary me-2">
                                <t t-esc="remainingCount"/> pendientes
                            </span>
                            <span class="badge bg-success">
                                <t t-esc="completedCount"/> completadas
                            </span>
                        </div>
                    </div>
    
                    <!-- Añadir Nuevo Todo -->
                    <div class="add-todo mb-3">
                        <div class="input-group">
                            <input type="text" 
                                   class="form-control" 
                                   placeholder="¿Qué necesitas hacer?"
                                   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> Añadir Tarea
                            </button>
                        </div>
                    </div>
    
                    <!-- Lista de Todos -->
                    <t t-if="state.todos.length > 0">
                        <!-- Mensaje de éxito cuando todo esté completado -->
                        <t t-if="allCompleted">
                            <div class="alert alert-success">
                                <i class="fa fa-trophy"></i>
                                ¡Felicidades! ¡Has completado todas tus tareas!
                            </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 hay tareas aún</h4>
                            <p class="text-muted">¡Añade tu primera tarea arriba para empezar!</p>
                        </div>
                    </t>
                </div>
    
                <!-- Información de Depuración -->
                <div class="debug-info mt-4 p-3 border rounded" t-att-class="{ 'border-light': state.theme === 'dark' }">
                    <h5>Información de Depuración</h5>
                    <small class="text-muted">
                        <strong>Resumen del Estado:</strong><br/>
                        Contador: <t t-esc="state.counter"/>, 
                        Tema: <t t-esc="state.theme"/>, 
                        Todos: <t t-esc="state.todos.length"/>, 
                        Editando: <t t-esc="state.isEditing"/>
                    </small>
                </div>
            </div>
        </t>
    
    </templates>
    

    3. ¡Prueba la Magia!

    Ahora, actualiza tu módulo y ve el componente. Aquí está lo que experimentarás:

    1. Haz clic en los botones del contador: Observa el número cambiar instantáneamente, junto con el color y el mensaje
    2. Alterna el tema: Ve toda la interfaz cambiar entre modos claro y oscuro inmediatamente
    3. Edita tu nombre: Nota cómo aparece el campo de entrada y el encabezado se actualiza en tiempo real
    4. Añade todos: Observa nuevos elementos aparecer en la lista instantáneamente
    5. Marca/desmarca todos: Ve las estadísticas de finalización actualizarse automáticamente
    6. Elimina todos: Observa elementos desaparecer y las estadísticas recalcularse

    Lo notable: Nunca escribimos una sola línea de código para actualizar el DOM directamente. Solo cambiamos los datos, y OWL manejó todas las actualizaciones visuales automáticamente.


    El ciclo de reactividad, y la asignación que lo rompe.{width=100%}

    Entendiendo la Reactividad en Profundidad

    ¿Qué Desencadena un Re-render?

    OWL re-renderiza tu componente cuando:

    1. Las propiedades del estado cambian: this.state.counter++
    2. Mutaciones de arrays: this.state.items.push(), this.state.items.splice()
    3. Actualizaciones de propiedades de objetos: this.state.user.name = "Nuevo Nombre"
    4. Cambios de propiedades anidadas: this.state.settings.theme = "dark"

    ¿Qué NO Desencadena un Re-render?

    // Esto no funcionará - reemplazar el objeto de estado
    this.state = { counter: 5 }; // OWL pierde la conexión reactiva
    
    // Esto no funcionará - modificar objetos no reactivos
    const plainObject = { count: 0 };
    plainObject.count++; // No es reactivo
    
    // Estos funcionan - modificar el estado reactivo
    this.state.counter = 5;           // Asignación directa
    this.state.items.push(newItem);   // Mutación de array
    delete this.state.temporaryData;  // Eliminación de propiedad
    

    Rendimiento y Agrupación

    OWL es inteligente sobre el rendimiento. No re-renderiza después de cada cambio de estado. En su lugar:

    1. Agrupación: Múltiples cambios de estado en el mismo ciclo de ejecución JavaScript se agrupan en un solo re-render
    2. Diffing Eficiente: Solo las partes del DOM que realmente cambiaron se actualizan
    3. Rastreo de Dependencias: OWL sabe exactamente qué partes de la plantilla dependen de qué propiedades del estado
    // Esto causará solo UN re-render, no tres
    updateMultipleValues() {
      this.state.counter++;        // Cambio 1
      this.state.userName = "Bob"; // Cambio 2  
      this.state.theme = "dark";   // Cambio 3
      // OWL los agrupa y re-renderiza una vez
    }
    

    Patrones de Estado Avanzados

    Propiedades Computadas con Getters

    Crea valores derivados que se actualizan automáticamente cuando cambian sus dependencias:

    setup() {
      this.state = useState({
        firstName: "Juan",
        lastName: "Pérez",
        items: [/* ... */]
      });
    }
    
    // Estos getters automáticamente "recomputan" cuando el estado cambia
    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);
    }
    

    Usar en plantilla:

    <h3>¡Bienvenido, <t t-esc="fullName"/>!</h3>
    <p>Tienes <t t-esc="expensiveItems.length"/> elementos caros por valor de $<t t-esc="totalValue"/></p>
    

    Estructuras de Estado Complejas

    setup() {
      this.state = useState({
        user: {
          profile: {
            name: "Alicia",
            avatar: "/path/to/avatar.png",
            preferences: {
              theme: "dark",
              language: "es"
            }
          },
          permissions: ["read", "write"]
        },
        application: {
          currentView: "dashboard",
          loading: false,
          errors: []
        },
        data: {
          items: [],
          filters: {
            category: "all",
            sortBy: "name",
            ascending: true
          }
        }
      });
    }
    
    // Todos estos desencadenan re-renders:
    updateUserName(newName) {
      this.state.user.profile.name = newName; // Cambio de propiedad anidada
    }
    
    changeTheme(theme) {
      this.state.user.profile.preferences.theme = theme; // Cambio anidado profundo
    }
    
    addError(error) {
      this.state.application.errors.push(error); // Mutación de array
    }
    

    Validación de Estado y Restricciones

    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) {
      // Restringir contador entre 0 y 100
      this.state.counter = Math.max(0, Math.min(100, value));
    }
    

    Escapando de la Reactividad: markRaw y toRaw

    useState envuelve el objeto que le des en un proxy reactivo, para que OWL pueda notar cuándo se lee una propiedad (para rastrearla) y cuándo se escribe (para disparar un re-render). Normalmente eso es exactamente lo que quieres—pero no siempre.

    A veces un objeto no debería volverse reactivo en absoluto: un conjunto de datos grande que nunca mutarás en el lugar, o una instancia de una librería de terceros (una librería de gráficos, un widget de mapas) que gestiona su propio estado interno y no espera estar envuelta en un proxy. Envolverla de todos modos desperdicia rendimiento rastreando claves que nadie lee, e incluso puede romper librerías que hacen comparaciones de identidad (===) internamente, ya que un proxy nunca es === al objeto que envuelve.

    markRaw(objeto) le dice al sistema de reactividad de OWL que deje un objeto en paz. Si en algún momento queda anidado dentro de un objeto reactivo, se guarda y se devuelve tal cual—intacto, sin observar:

    import { Component, useState, markRaw } from "@odoo/owl";
    
    setup() {
      // chartInstance gestiona su propio estado interno; nunca queremos que
      // OWL observe sus propiedades ni la envuelva en un proxy.
      const chartInstance = markRaw(new ThirdPartyChart());
    
      this.state = useState({
        counter: 0,
        chart: chartInstance,
      });
    
      // Mutar las propias propiedades de chart no disparará un re-render—tal como se busca.
      this.state.chart.zoomLevel = 2;
    }
    

    toRaw(objetoReactivo) hace el trabajo opuesto: dado un objeto reactivo (o una parte de uno), devuelve el objeto original, sin proxy, que está por debajo. Esto es útil para comparaciones de identidad (obj === toRaw(state.obj) puede ser true incluso cuando obj === state.obj es false, ya que state.obj es un proxy) y para depurar—loguear un objeto sin proxy es más fácil de leer que loguear un proxy.

    const original = { name: "Alice" };
    const state = useState({ user: original });
    
    console.log(original === state.user);          // false — state.user es un proxy
    console.log(original === toRaw(state.user));    // true
    

    No recurrirás a ninguno de los dos muy seguido, pero vale la pena conocerlos para el día que integres un gráfico, un mapa, o cualquier otra librería que gestione su propio estado.


    Errores Comunes y Soluciones

    Error 1: Perder Reactividad

    // Incorrecto - Esto rompe la reactividad
    someMethod() {
      this.state = { items: [1, 2, 3] }; // Reemplaza completamente el objeto reactivo
    }
    
    // Correcto - Modifica propiedades, no reemplaces el objeto
    someMethod() {
      this.state.items = [1, 2, 3]; // Modifica el estado reactivo
    }
    

    Error 2: Olvidar useState

    // Incorrecto - objeto plano: los cambios son invisibles para OWL, la UI nunca se actualiza
    setup() {
      this.state = { counter: 0 };
    }
    
    // Correcto - objeto reactivo: los cambios disparan re-renders
    setup() {
      this.state = useState({ counter: 0 });
    }
    

    Error 3: Actualizaciones de Estado Asíncronas

    // Posibles condiciones de carrera
    async fetchData() {
      this.state.loading = true;
      const data = await this.service.getData();
      this.state.data = data;
      this.state.loading = false; // ¿Y si el componente fue destruido mientras tanto?
    }
    
    // Mejor enfoque: el helper status() de OWL te dice si el componente sigue vivo
    import { status } from "@odoo/owl";
    
    async fetchData() {
      this.state.loading = true;
      try {
        const data = await this.service.getData();
        if (status(this) === "destroyed") return; // El componente ya no existe, detente aquí
        this.state.data = data;
      } finally {
        if (status(this) !== "destroyed") {
          this.state.loading = false;
        }
      }
    }
    

    Estado vs Props: Cuándo Usar Cada Uno

    Usa useState cuando: - Los datos pertenecen a este componente - Los datos cambian basándose en interacciones del usuario en este componente - Los datos son temporales o específicos de la sesión - Necesitas modificar los datos

    Usa Props cuando: - Los datos vienen de un componente padre - Los datos son configuración o ajustes - Múltiples componentes necesitan mostrar los mismos datos - Los datos son de solo lectura para este componente

    // Estado: Datos propiedad del componente
    this.state = useState({
      inputValue: "",        // Usuario escribiendo en este componente
      isLoading: false,      // Estado de carga de este componente
      showModal: false       // Estado de UI de este componente
    });
    
    // Props: Datos proporcionados por el padre
    static props = {
      userId: { type: Number },      // Configuración del padre
      readonly: { type: Boolean },   // Configuración de comportamiento
      initialData: { type: Array }   // Datos a mostrar
    };
    

    Depurando Estado Reactivo

    Consejos de Herramientas de Desarrollo del Navegador

    1. Owl DevTools: Instala la extensión de navegador de Owl para inspección de componentes y estado
    2. Console Logging: Añade declaraciones console.log estratégicas en métodos
    3. Breakpoints: Establece breakpoints en métodos que cambian el estado
    4. Snapshots de Estado: Registra todo el estado en momentos clave
    // Método auxiliar de depuración
    debugState(action) {
      console.group(`Cambio de Estado: ${action}`);
      console.log("Estado actual:", JSON.parse(JSON.stringify(this.state)));
      console.log("Componente:", this);
      console.groupEnd();
    }
    
    // Usar en métodos
    increment() {
      this.state.counter++;
      this.debugState("increment");
    }
    

    Escenarios Comunes de Depuración

    <!-- Añadir depuración temporal a plantillas -->
    <div class="debug-panel" style="position: fixed; top: 10px; right: 10px; background: yellow; padding: 10px;">
      <pre t-esc="JSON.stringify(state, null, 2)"/>
    </div>
    
    // Método para reiniciar estado para pruebas
    resetToInitialState() {
      Object.assign(this.state, {
        counter: 0,
        userName: "Invitado",
        todos: [],
        // ... otros valores iniciales
      });
    }
    

    La Mentalidad Declarativa

    Este capítulo te introdujo al cambio fundamental de pensamiento que requieren los frameworks de UI modernos:

    En lugar de pensar: "Cuando el usuario haga clic en este botón, necesito encontrar el elemento contador en el DOM y actualizar su texto"

    Piensa: "Cuando el usuario haga clic en este botón, necesito actualizar el valor del contador en mi estado, y la UI automáticamente reflejará este cambio"

    Este enfoque declarativo hace que tu código sea:

    • Más predecible: La UI siempre es una función de tu estado
    • Más fácil de depurar: Puedes inspeccionar el estado para entender la UI
    • Más mantenible: No hay manipulación manual del DOM de la que hacer seguimiento
    • Más testeable: Puedes probar cambios de estado sin involucrar el DOM

    TL;DR: Envuelve los datos en useState() para hacerlos reactivos; muta sus propiedades directamente (state.counter++) en vez de reasignar todo el objeto, y la plantilla se vuelve a renderizar automáticamente — cambias el dato, OWL actualiza el DOM.

    Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch7_ex1. Las instrucciones de instalación están en el README del repositorio.

    Ejercicios

    1. Añade un botón de reinicio total. Añade un botón que reinicie counter a 0, todos a un array vacío, y userName de vuelta a "Usuario Anónimo"—todo en un solo método—y confirma que toda la UI se actualiza a la vez.
    2. Provoca el Error 1 a propósito. En reset(), reemplaza temporalmente this.state.counter = 0 por this.state = { counter: 0 } (como se muestra en el Error 1 arriba). Haz clic en el botón y observa que la UI silenciosamente deja de actualizarse. Deshaz el cambio y confirma que vuelve a funcionar.
    3. Añade una propiedad computada. Añade un nuevo getter, oldestPendingTodo, que devuelva la primera tarea en state.todos donde completed sea false (o null si no hay ninguna), y muéstrala en la plantilla.

    ¿Qué Sigue?

    El estado reactivo es genial para datos que un componente posee, pero las aplicaciones reales están hechas de componentes que se comunican entre sí. El Capítulo 8 cubre las props, el mecanismo para pasar datos de un componente padre hacia sus hijos.

    en Libro OWL
    Capítulo 8: Props: Comunicación de Padre a Hijo
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Enlaces útiles
    • Inicio
    • Sobre nosotros
    • Blog
    • El libro de OWL 2.0
    • Ayuda
    • Contáctanos
    Sobre nosotros

    Simplify It S.R.L. es una firma de implementación y desarrollo Odoo con base en La Paz, Bolivia, que trabaja con empresas de Latinoamérica y Norteamérica. Desarrollamos sobre Odoo desde la versión 6.1: módulos a medida, migraciones de versión y localización boliviana.

    También somos autores del libro de OWL 2.0, que publicamos gratis capítulo a capítulo en nuestro blog.

    Conecta con nosotros
    • Contáctanos
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Síguenos
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Con la tecnología de Odoo - Cree un sitio web gratuito