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 11: Hooks Modernos - useEffect y useRef

  • Todos los blogs
  • Libro OWL
  • Capítulo 11: Hooks Modernos - useEffect y useRef
  • 25 de agosto de 2026 por
    Capítulo 11: Hooks Modernos - useEffect y useRef
    Grover Menacho

    ¿Por qué este capítulo? Los hooks de ciclo de vida por sí solos te empujan a escribir código organizado por "cuándo", no por "por qué" — la configuración de un timer termina en onMounted, su limpieza en onWillUnmount, y ambos se separan a medida que el componente crece. Los hooks modernos te permiten mantener todo lo relacionado con una responsabilidad junto. ¿Qué trata de resolver Odoo con esto? Los propios componentes del backend de Odoo manejan muchos recursos externos — mediciones del DOM, suscripciones, timers, manejo de foco — y useEffect/useRef dan un lugar único y predecible para configurarlos y limpiarlos sin esparcir esa lógica entre varios métodos de ciclo de vida. Aplicación en la vida real: Cajas de búsqueda con debounce, foco automático al abrir un diálogo, y sincronizar una librería de gráficos con el estado del componente son cosas que he entregado en addons de clientes usando exactamente los patrones de este capítulo.

    En el Capítulo 10, exploramos los hooks de ciclo de vida como onMounted, onWillStart y onWillUnmount. Estos hooks son poderosos e intuitivos, pero el desarrollo moderno en OWL fomenta un enfoque más funcional y flexible usando hooks modernos, principalmente useEffect y useRef.

    Los hooks modernos representan un cambio de paradigma en cómo pensamos sobre la lógica de componentes. En lugar de organizar el código por eventos del ciclo de vida ("cuando el componente se monta, haz esto"), lo organizamos por características o responsabilidades ("todo lo relacionado con este temporizador vive junto"). Este enfoque hace que el código sea más mantenible, comprobable y más fácil de razonar.

    Profundicemos en estos hooks modernos y aprendamos cómo pueden hacer tus componentes más poderosos y elegantes.


    useRef: Más allá de las Referencias DOM

    Encontramos brevemente useRef en el Capítulo 10 para acceder a elementos DOM. Aunque el acceso al DOM es su caso de uso más común, useRef es en realidad un hook mucho más versátil para crear referencias estables a cualquier valor.

    Entendiendo las Refs

    En OWL, una ref es un objeto con una sola propiedad .el que apunta al elemento DOM marcado con la directiva t-ref correspondiente en tu plantilla. La característica clave de una ref es que es estable a través de re-renderizados—siempre apunta a la misma instancia de objeto, y .el se mantiene actualizada conforme el DOM cambia.

    ¿Vienes de React? OWL no tiene "value refs" con una propiedad .current. No las necesita: los componentes OWL son instancias de clase, así que para valores mutables que no deben disparar re-renders (IDs de temporizadores, valores anteriores, instancias de librerías) simplemente usas propiedades de instancia normales como this.timer. En OWL, useRef es exclusivamente para acceder al DOM.

    Referencias DOM: El Caso de Uso Clásico

    Comencemos con el patrón familiar de referencia DOM:

    JavaScript (focus_manager.js):

    import { Component, useRef, useState } from "@odoo/owl";
    
    export class FocusManager extends Component {
      static template = "my_module.FocusManager";
    
      setup() {
        this.state = useState({ 
          inputValue: "",
          focusCount: 0 
        });
    
        // Crear refs para múltiples elementos
        this.nameInputRef = useRef("name-input");
        this.emailInputRef = useRef("email-input");
        this.submitButtonRef = useRef("submit-button");
      }
    
      focusName() {
        if (this.nameInputRef.el) {
          this.nameInputRef.el.focus();
          this.state.focusCount++;
          console.log("FocusManager: Enfocado input de nombre");
        }
      }
    
      focusEmail() {
        if (this.emailInputRef.el) {
          this.emailInputRef.el.focus();
          this.state.focusCount++;
          console.log("FocusManager: Enfocado input de email");
        }
      }
    
      focusSubmit() {
        if (this.submitButtonRef.el) {
          this.submitButtonRef.el.focus();
          this.state.focusCount++;
          console.log("FocusManager: Enfocado botón de envío");
        }
      }
    
      handleKeyDown(event) {
        // Navegar entre campos con Tab
        if (event.key === "Tab" && event.shiftKey) {
          // Manejar navegación personalizada con tab si es necesario
          console.log("FocusManager: Shift+Tab presionado");
        }
      }
    
      onInputChange(event) {
        this.state.inputValue = event.target.value;
    
        // Auto-enfocar siguiente campo cuando el actual esté lleno
        if (event.target === this.nameInputRef.el && event.target.value.length > 2) {
          setTimeout(() => this.focusEmail(), 100);
        }
      }
    }
    

    Template (focus_manager.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
        <t t-name="my_module.FocusManager" owl="1">
            <div class="focus-manager card">
                <div class="card-header">
                    <h5>Demo de Gestor de Enfoque</h5>
                    <small class="text-muted">Eventos de enfoque: <t t-esc="state.focusCount"/></small>
                </div>
    
                <div class="card-body">
                    <form t-on-keydown="handleKeyDown">
                        <div class="mb-3">
                            <label for="name" class="form-label">Nombre</label>
                            <input 
                                type="text" 
                                class="form-control" 
                                t-ref="name-input"
                                placeholder="Ingresa tu nombre"
                                t-model="state.inputValue"
                                t-on-input="onInputChange"
                            />
                        </div>
    
                        <div class="mb-3">
                            <label for="email" class="form-label">Email</label>
                            <input 
                                type="email" 
                                class="form-control" 
                                t-ref="email-input"
                                placeholder="Ingresa tu email"
                            />
                        </div>
    
                        <button 
                            type="submit" 
                            class="btn btn-primary me-2"
                            t-ref="submit-button">
                            Enviar
                        </button>
                    </form>
    
                    <div class="mt-3">
                        <small class="text-muted">Enfoque Rápido:</small><br/>
                        <button class="btn btn-sm btn-outline-secondary me-1" t-on-click="focusName">
                            Enfocar Nombre
                        </button>
                        <button class="btn btn-sm btn-outline-secondary me-1" t-on-click="focusEmail">
                            Enfocar Email
                        </button>
                        <button class="btn btn-sm btn-outline-secondary" t-on-click="focusSubmit">
                            Enfocar Envío
                        </button>
                    </div>
                </div>
            </div>
        </t>
    </templates>
    

    Almacenando Datos Mutables: Propiedades de Instancia Normales

    ¿Y qué pasa con los valores mutables que no deberían desencadenar re-renderizados cuando cambian—IDs de temporizadores, valores anteriores para comparación, instancias de librerías de terceros? En OWL no necesitas ningún hook para esto. Como tu componente es una instancia de clase, una propiedad normal hace el trabajo:

    Ejemplo: Temporizador con Propiedades de Instancia

    import { Component, useState } from "@odoo/owl";
    
    export class TimerComponent extends Component {
      static template = "my_module.TimerComponent";
    
      setup() {
        this.state = useState({ 
          seconds: 0,
          isRunning: false 
        });
    
        // Propiedades de instancia normales - cambiarlas nunca causa un re-render
        this.timer = null;            // ID del temporizador
        this.previousSeconds = 0;     // valor anterior para comparación
        this.startCount = 0;          // cuántas veces se inició el temporizador
      }
    
      startTimer() {
        if (this.state.isRunning) return;
    
        console.log("TimerComponent: Iniciando temporizador");
        this.state.isRunning = true;
        this.startCount += 1;
    
        // Almacenar el ID del temporizador en la instancia - esto no desencadenará re-renderizado
        this.timer = setInterval(() => {
          this.state.seconds++;
    
          // Registrar cada 10 segundos usando comparación de valor anterior
          if (this.state.seconds % 10 === 0 && this.state.seconds !== this.previousSeconds) {
            console.log(`TimerComponent: ${this.state.seconds} segundos transcurridos`);
            this.previousSeconds = this.state.seconds;
          }
        }, 1000);
      }
    
      stopTimer() {
        if (!this.state.isRunning) return;
    
        console.log("TimerComponent: Deteniendo temporizador");
        this.state.isRunning = false;
    
        // Limpiar temporizador usando ID almacenado
        if (this.timer) {
          clearInterval(this.timer);
          this.timer = null;
        }
      }
    
      resetTimer() {
        this.stopTimer();
        this.state.seconds = 0;
        this.previousSeconds = 0;
        console.log("TimerComponent: Temporizador reiniciado");
      }
    
      get timerStats() {
        return {
          currentTime: this.formatTime(this.state.seconds),
          startCount: this.startCount,
          isActive: this.timer !== null
        };
      }
    
      formatTime(seconds) {
        const mins = Math.floor(seconds / 60);
        const secs = seconds % 60;
        return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
      }
    }
    

    La regla general: los datos reactivos que la plantilla muestra van en useState; todo lo demás es una propiedad de instancia normal; useRef es solo para elementos DOM.


    useEffect: La Navaja Suiza de los Efectos Secundarios

    El hook useEffect es el hook más poderoso y flexible en OWL moderno. Reemplaza múltiples hooks de ciclo de vida con una sola API unificada que agrupa la lógica relacionada.

    ¿Qué es un "efecto secundario"? Es cualquier cosa que hace un código que va más allá de calcular su valor de retorno: iniciar un temporizador, agregar un listener de eventos, llamar al servidor, cambiar document.title. El renderizado debería ser un cálculo puro de "estado entra, DOM sale"; useEffect es donde va todo lo que no lo es.

    Entendiendo las Dependencias

    La magia de useEffect radica en su segundo parámetro: una función de dependencias que devuelve la lista de valores de los que depende el efecto. OWL llama a esta función en cada renderizado y re-ejecuta el efecto solo cuando alguno de los valores devueltos cambió.

    Cuidado si conoces React: en React las dependencias son un array plano ([state.count]). En OWL son una función que devuelve un array (() => [state.count]). Pasar un array plano es un bug de migración muy común.

    // Se ejecuta una vez después del montaje, limpieza al desmontar (como onMounted + onWillUnmount)
    useEffect(
      () => {
        console.log("Componente montado");
      },
      () => []
    );
    
    // Se ejecuta después de cada renderizado (generalmente no es lo que quieres) - omite la función de dependencias
    useEffect(() => {
      console.log("Componente renderizado");
    });
    
    // Se ejecuta cuando valores específicos cambian
    useEffect(
      () => {
        console.log("Count o name cambiaron");
      },
      () => [this.state.count, this.state.name]
    );
    

    Patrón 1: Efectos Solo de Montaje (Reemplazando onMounted)

    Creemos un componente que configura múltiples cosas cuando se monta:

    JavaScript (dashboard.js):

    import { Component, useEffect, useRef, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class Dashboard extends Component {
      static template = "my_module.Dashboard";
    
      setup() {
        this.state = useState({
          currentTime: new Date().toLocaleTimeString(),
          stats: {
            users: 0,
            orders: 0,
            revenue: 0
          },
          loading: true
        });
    
        this.orm = useService("orm");
        this.titleRef = useRef("page-title");
    
        // Efecto solo de montaje - se ejecuta una vez después del montaje del componente
        useEffect(() => {
          console.log("Dashboard: Componente montado, inicializando...");
    
          // 1. Configurar reloj
          const clockInterval = setInterval(() => {
            this.state.currentTime = new Date().toLocaleTimeString();
          }, 1000);
    
          // 2. Enfocar el título
          if (this.titleRef.el) {
            this.titleRef.el.focus();
          }
    
          // 3. Cargar datos iniciales
          this.loadDashboardData();
    
          // 4. Configurar listener de enfoque de ventana
          const handleWindowFocus = () => {
            console.log("Dashboard: Ventana enfocada, actualizando datos");
            this.loadDashboardData();
          };
    
          window.addEventListener('focus', handleWindowFocus);
    
          // Función de limpieza - se ejecuta cuando el componente se desmonta
          return () => {
            console.log("Dashboard: Limpiando efectos de montaje");
            clearInterval(clockInterval);
            window.removeEventListener('focus', handleWindowFocus);
          };
        }, () => []); // Lista de dependencias vacía = ejecutar una vez en el montaje
      }
    
      async loadDashboardData() {
        try {
          console.log("Dashboard: Cargando estadísticas del dashboard");
    
          // Simular carga de múltiples estadísticas en paralelo
          const [users, orders] = await Promise.all([
            this.orm.searchCount("res.users", []),
            this.orm.searchCount("sale.order", [["state", "=", "sale"]])
          ]);
    
          this.state.stats = {
            users,
            orders,
            revenue: orders * 150 // Cálculo de ingresos simulado
          };
    
          this.state.loading = false;
          console.log("Dashboard: Estadísticas cargadas exitosamente");
    
        } catch (error) {
          console.error("Dashboard: Falló la carga de estadísticas:", error);
          this.state.loading = false;
        }
      }
    }
    

    Patrón 2: Efectos Reactivos (Reemplazando onWillUpdateProps)

    Los efectos pueden reaccionar a cambios específicos de estado o props:

    JavaScript (user_profile.js):

    import { Component, useEffect, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class UserProfile extends Component {
      static template = "my_module.UserProfile";
      static props = { 
        userId: { type: Number } 
      };
    
      setup() {
        this.state = useState({
          user: null,
          loading: false,
          error: null,
          loadCount: 0
        });
    
        this.orm = useService("orm");
    
        // Efecto que reacciona a cambios en la prop userId
        useEffect(() => {
          if (!this.props.userId) {
            console.log("UserProfile: No se proporcionó ID de usuario");
            this.state.user = null;
            return;
          }
    
          console.log(`UserProfile: Cargando datos de usuario para ID ${this.props.userId}`);
          this.loadUserData(this.props.userId);
    
        }, () => [this.props.userId]); // Se ejecuta cuando userId cambia
    
        // Efecto separado para registrar intentos de carga
        useEffect(() => {
          if (this.state.loadCount > 0) {
            console.log(`UserProfile: Intento de carga #${this.state.loadCount}`);
          }
        }, () => [this.state.loadCount]);
      }
    
      async loadUserData(userId) {
        this.state.loading = true;
        this.state.error = null;
        this.state.loadCount += 1;
    
        try {
          const users = await this.orm.read("res.users", [userId], [
            "name", 
            "email", 
            "phone", 
            "partner_id",
            "login_date"
          ]);
    
          if (users.length > 0) {
            this.state.user = users[0];
            console.log(`UserProfile: Cargado exitosamente ${users[0].name}`);
          } else {
            throw new Error(`Usuario ${userId} no encontrado`);
          }
    
        } catch (error) {
          console.error(`UserProfile: Falló la carga del usuario ${userId}:`, error);
          this.state.error = error.message;
          this.state.user = null;
        } finally {
          this.state.loading = false;
        }
      }
    }
    

    Patrón 3: Múltiples Efectos Independientes

    Una de las mayores ventajas de useEffect es que puedes tener múltiples efectos, cada uno manejando una responsabilidad específica:

    JavaScript (notification_center.js):

    Primero, el componente configura su estado y su primer efecto, que solo se preocupa de si el navegador está conectado:

    import { Component, useEffect, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    
    export class NotificationCenter extends Component {
      static template = "my_module.NotificationCenter";
    
      setup() {
        this.state = useState({
          notifications: [],
          connectionStatus: 'connecting',
          lastUpdate: null,
          unreadCount: 0
        });
    
        this.orm = useService("orm");
    
        // Efecto 1: Gestión de conexión
        useEffect(() => {
          console.log("NotificationCenter: Configurando monitoreo de conexión");
    
          const checkConnection = () => {
            this.state.connectionStatus = navigator.onLine ? 'connected' : 'disconnected';
          };
    
          // Verificación inicial
          checkConnection();
    
          // Configurar listeners
          window.addEventListener('online', checkConnection);
          window.addEventListener('offline', checkConnection);
    
          return () => {
            window.removeEventListener('online', checkConnection);
            window.removeEventListener('offline', checkConnection);
          };
        }, () => []);
    

    Después, un segundo efecto observa connectionStatus y solo empieza a sondear notificaciones nuevas cuando el navegador realmente está conectado — nota que su función de dependencias devuelve [this.state.connectionStatus], así que se re-ejecuta cada vez que ese valor cambia:

        // Efecto 2: Actualización periódica de notificaciones
        useEffect(() => {
          if (this.state.connectionStatus !== 'connected') {
            console.log("NotificationCenter: Saltando actualización - no conectado");
            return;
          }
    
          console.log("NotificationCenter: Configurando actualización periódica");
    
          const refreshInterval = setInterval(() => {
            this.refreshNotifications();
          }, 30000); // Actualizar cada 30 segundos
    
          // Carga inicial
          this.refreshNotifications();
    
          return () => {
            clearInterval(refreshInterval);
          };
        }, () => [this.state.connectionStatus]); // Re-ejecutar cuando cambie el estado de conexión
    

    Finalmente, un tercer efecto, completamente independiente, recalcula el contador de no leídas y mantiene sincronizado el título de la pestaña cada vez que cambia la lista de notificaciones — no sabe ni le importa la lógica de conexión de arriba:

        // Efecto 3: Cálculo de contador de no leídas
        useEffect(() => {
          const unread = this.state.notifications.filter(n => !n.is_read).length;
          this.state.unreadCount = unread;
    
          console.log(`NotificationCenter: ${unread} notificaciones no leídas`);
    
          // Actualizar título del navegador si hay notificaciones no leídas
          if (unread > 0) {
            document.title = `(${unread}) Odoo - Notificaciones`;
          } else {
            document.title = "Odoo";
          }
    
          return () => {
            // Limpiar título en el desmontaje
            document.title = "Odoo";
          };
        }, () => [this.state.notifications.length]);
      }
    
      async refreshNotifications() {
        try {
          console.log("NotificationCenter: Actualizando notificaciones");
    
          const notifications = await this.orm.searchRead(
            "mail.notification",
            [["res_partner_id", "=", this.currentUserId]],
            ["id", "mail_message_id", "is_read", "create_date"],
            { 
              order: "create_date desc", 
              limit: 50 
            }
          );
    
          this.state.notifications = notifications;
          this.state.lastUpdate = new Date();
    
        } catch (error) {
          console.error("NotificationCenter: Falló la actualización de notificaciones:", error);
        }
      }
    
      get currentUserId() {
        // Esto vendría de un servicio de usuario en una app real
        return 1; // Simplificado para el ejemplo
      }
    }
    

    Tres efectos, tres responsabilidades, cero lógica enredada — ese es el resultado de organizar por característica en vez de por momento del ciclo de vida.

    Patrones Avanzados de useEffect

    Patrón: Efecto con Lógica Asíncrona

    useEffect(() => {
      let cancelled = false;
    
      const fetchData = async () => {
        try {
          const result = await this.orm.searchRead("some.model", [], []);
    
          // Solo actualizar estado si el efecto no ha sido cancelado
          if (!cancelled) {
            this.state.data = result;
          }
        } catch (error) {
          if (!cancelled) {
            console.error("Falló la obtención:", error);
          }
        }
      };
    
      fetchData();
    
      // Limpieza: marcar como cancelado para prevenir actualizaciones de estado
      return () => {
        cancelled = true;
      };
    }, () => [this.state.someDependency]);
    

    Patrón: Efecto con Debounce

    Debounce (o "postergar") significa retrasar un trabajo hasta que una ráfaga de cambios que lo disparan se detenga durante un tiempo determinado — en vez de buscar en cada tecla presionada, esperas a que el usuario deje de escribir por 300ms.

    useEffect(() => {
      const timeoutId = setTimeout(() => {
        // Realizar operación costosa solo después de que el valor haya estado estable por 300ms
        this.performSearch(this.state.searchTerm);
      }, 300);
    
      return () => {
        clearTimeout(timeoutId);
      };
    }, () => [this.state.searchTerm]);
    

    Combinando useRef y useEffect: Patrones Poderosos

    Cuando combinas useRef y useEffect, desbloqueas algunos patrones muy poderosos:

    Patrón: Comparación de Valor Anterior

    setup() {
      this.state = useState({ count: 0 });
      this.previousCount = undefined; // propiedad de instancia normal
    
      useEffect(
        () => {
          console.log(`Count cambió de ${this.previousCount} a ${this.state.count}`);
    
          // Almacenar valor actual para próxima comparación
          this.previousCount = this.state.count;
        },
        () => [this.state.count]
      );
    }
    

    Patrón: Integración con Biblioteca de Terceros

    setup() {
      this.chartCanvasRef = useRef("chart-canvas"); // ref DOM (t-ref="chart-canvas")
      this.chart = null;                            // propiedad normal para la instancia de la librería
      this.state = useState({ data: [] });
    
      // Configurar gráfico en el montaje
      useEffect(
        () => {
          if (!this.chartCanvasRef.el) return;
    
          const ctx = this.chartCanvasRef.el.getContext('2d');
          this.chart = new Chart(ctx, {
            type: 'line',
            data: { datasets: [] }
          });
    
          return () => {
            if (this.chart) {
              this.chart.destroy();
            }
          };
        },
        () => []
      );
    
      // Actualizar gráfico cuando los datos cambien
      useEffect(
        () => {
          if (this.chart) {
            this.chart.data = this.processChartData();
            this.chart.update();
          }
        },
        () => [this.state.data.length]
      );
    }
    

    Guía de Migración: De Hooks de Ciclo de Vida a Hooks Modernos

    Si tienes componentes existentes usando hooks de ciclo de vida, aquí tienes cómo migrarlos:

    Antes: Hooks de Ciclo de Vida

    setup() {
      this.state = useState({ data: null });
      this.orm = useService("orm");
    
      onWillStart(async () => {
        this.state.data = await this.orm.searchRead("model", [], []);
      });
    
      onMounted(() => {
        document.title = "Mi Componente";
        this.timer = setInterval(this.refresh, 5000);
      });
    
      onWillUnmount(() => {
        document.title = "Odoo";
        if (this.timer) {
          clearInterval(this.timer);
        }
      });
    }
    

    Después: Hooks Modernos

    setup() {
      this.state = useState({ data: null });
      this.orm = useService("orm");
    
      // Mantén onWillStart para datos pre-renderizado: useEffect se ejecuta DESPUÉS
      // del montaje, así que reemplazarlo con un efecto traería de vuelta el parpadeo
      onWillStart(async () => {
        this.state.data = await this.orm.searchRead("model", [], []);
      });
    
      // Efectos de montaje (reemplaza onMounted + onWillUnmount)
      useEffect(
        () => {
          document.title = "Mi Componente";
          const timer = setInterval(this.refresh, 5000);
    
          return () => {
            document.title = "Odoo";
            clearInterval(timer);
          };
        },
        () => []
      );
    }
    

    Nota que onWillStart se queda: es el único hook que puede retrasar el primer renderizado, así que no tiene equivalente en useEffect.


    Hooks de Entorno: useEnv, useSubEnv, useChildSubEnv

    Las props son geniales para pasar datos un nivel hacia abajo, de un padre a su hijo directo. Pero ¿qué pasa con datos que docenas de componentes esparcidos por todo el árbol necesitan—el usuario actual, una función de traducción, feature flags? Pasarlos como prop a través de cada componente intermedio (prop drilling, el mismo problema que motiva el store de estado global del Capítulo 15) se vuelve doloroso rápidamente.

    La respuesta de OWL es el entorno (env): un objeto plano compartido por todos los componentes del árbol, definido una vez al montar la aplicación y disponible en todas partes sin tener que pasarlo por props.

    useEnv() lee el entorno del componente actual:

    import { Component, useEnv } from "@odoo/owl";
    
    class UserBadge extends Component {
        static template = xml`<span t-esc="env.currentUser.name"/>`;
    
        setup() {
            this.env = useEnv();
        }
    }
    

    A veces un componente quiere agregar o sobrescribir algo en el entorno para su propio subárbol—un tema, una configuración específica—sin cambiarlo para toda la app. Dos hooks hacen esto, y la diferencia entre ellos importa:

    • useSubEnv(nuevosValores) — mezcla nuevosValores en el entorno para este componente y todos sus hijos.
    • useChildSubEnv(nuevosValores) — mezcla nuevosValores en el entorno solo para los hijos, dejando el env propio de este componente sin tocar.
    setup() {
        // Todo componente por debajo de este (hijos, nietos, ...) ahora ve
        // env.theme === "dark". Este componente también, porque useSubEnv
        // también afecta a quien lo llama.
        useSubEnv({ theme: "dark" });
    }
    

    Usa useChildSubEnv cuando un componente quiere configurar a sus descendientes sin implicar que la misma configuración le aplica a sí mismo—por ejemplo, un componente <Section title="..."> que define un nivel de encabezado para sus hijos sin convertirse él mismo en un encabezado.

    useExternalListener: Escuchar Fuera del Componente

    El Capítulo 1 agregó un listener de click a mano con addEventListener, y hubo que recordar llamar removeEventListener en onWillUnmount para evitar una fuga. useExternalListener hace ambos pasos por ti en una sola llamada, que es exactamente lo que quieres para escuchar en window o document—objetivos fuera del DOM propio del componente que OWL no gestiona.

    import { Component, useExternalListener } from "@odoo/owl";
    
    class DropdownMenu extends Component {
        setup() {
            // Se agrega automáticamente al montar y se quita al desmontar—
            // sin par onMounted/onWillUnmount que escribir ni olvidar.
            useExternalListener(window, "click", this.closeIfOutside.bind(this));
        }
    
        closeIfOutside(ev) {
            if (!this.el.contains(ev.target)) {
                this.state.open = false;
            }
        }
    }
    

    Recibe el objetivo (window, document, o cualquier nodo DOM), el nombre del evento y un manejador, y reenvía cualquier argumento extra a addEventListener—así que useExternalListener(window, "keydown", this.onKeyDown, { capture: true }) también funciona.

    Otro hook que vale la pena conocer, aunque rara vez lo llamarás directamente: useComponent() devuelve la instancia del componente actual. Existe principalmente como bloque de construcción para escribir tus propios hooks personalizados que necesiten acceder al componente que los llama—el código de aplicación casi nunca lo necesita.


    Mejores Prácticas y Errores Comunes

    Qué Hacer

    1. Agrupar lógica relacionada - Poner configuración y limpieza para la misma característica en un efecto
    2. Usar múltiples efectos - Separar responsabilidades en diferentes efectos
    3. Incluir todas las dependencias - La función de dependencias debe devolver todos los valores reactivos que el efecto usa
    4. Retornar funciones de limpieza - Prevenir memory leaks limpiando recursos
    5. Usar el almacenamiento correcto - Datos reactivos de la plantilla en useState, valores mutables no reactivos (IDs de temporizadores, valores anteriores, instancias de librerías) como propiedades de instancia normales, useRef solo para elementos DOM

    Qué No Hacer

    1. No pasar un array plano como dependencias - OWL espera una función que devuelve un array (() => [..]), no el array en sí
    2. No usar efectos para todo - Alguna lógica pertenece a event handlers, no a efectos
    3. No reemplazar onWillStart con un efecto - Los efectos se ejecutan después del montaje; solo onWillStart puede retrasar el primer renderizado
    4. No sobre-complicar - A veces los hooks de ciclo de vida son más claros para casos simples

    Error Común: Array en Lugar de Función

    // MALO: array plano estilo React - OWL no rastreará estas dependencias
    useEffect(() => {
      this.syncWithServer(this.state.filter);
    }, [this.state.filter]);
    
    // BUENO: una función que devuelve el array de dependencias
    useEffect(
      () => {
        this.syncWithServer(this.state.filter);
      },
      () => [this.state.filter]
    );
    

    ¿Por qué una función? OWL la llama en cada renderizado para obtener valores frescos y compararlos con los anteriores. Un array plano se evaluaría una sola vez, cuando setup() se ejecutó.

    Error Común: Esperar que useSubEnv No Afecte a Quien lo Llama

    // El env.theme de ESTE MISMO componente también se vuelve "dark" aquí,
    // no solo el de sus hijos—useSubEnv siempre incluye a quien lo llama.
    useSubEnv({ theme: "dark" });
    

    Si quieres configurar a los descendientes sin cambiar nada para el componente actual, usa useChildSubEnv en su lugar. Confundir uno con el otro es un bug fácil de pasar por alto: todo lo que está por debajo del componente se comporta correctamente, pero el renderizado del propio componente cambia de forma inesperada.


    Resumen

    Los hooks modernos representan una evolución significativa en cómo escribimos componentes:

    • useRef crea referencias estables a elementos DOM (y las propiedades de instancia cubren cualquier otro valor mutable)
    • useEffect maneja todos los efectos secundarios con control preciso sobre cuándo se ejecutan
    • Las funciones de dependencias hacen que los efectos sean predecibles y eficientes
    • Funciones de limpieza previenen memory leaks y conflictos de recursos

    La perspectiva clave es organizacional: en lugar de pensar "qué pasa cuando el componente se monta", piensa "qué necesita esta característica para funcionar correctamente". Agrupa toda la lógica para cada característica—configuración, actualizaciones y limpieza—en efectos enfocados.

    Este enfoque hace que tus componentes sean más mantenibles, comprobables y más fáciles de entender. En el próximo capítulo, exploraremos cómo comunicarnos con el servidor usando servicios, construyendo sobre la sólida base de hooks modernos que hemos establecido aquí.

    TL;DR: useRef te da un identificador estable a un elemento DOM o un valor mutable; useEffect ejecuta (y limpia) efectos secundarios según una función de dependencias explícita, así que agrupas la configuración y el desmontaje de una responsabilidad en un solo lugar en vez de esparcirlos entre hooks de ciclo de vida.

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


    Ejercicios

    1. Enfocar al montar: Construye un componente pequeño con un input de texto y un useRef. Usa useEffect con una función de dependencias vacía (() => []) para enfocar el input tan pronto como el componente se monte.
    2. Corrige el bug de dependencias: Toma este fragmento roto y corrígelo para que el efecto realmente se re-ejecute cuando state.query cambie: javascript useEffect(() => { console.log("Buscando", this.state.query); }, [this.state.query]);
    3. Contador con debounce: Agrega un useEffect que registre "¡Estable!" en la consola solo después de que state.count no haya cambiado durante 500ms (pista: setTimeout en el efecto, clearTimeout en la función de limpieza — la misma forma que el ejemplo de búsqueda con debounce de arriba).

    ¿Qué Sigue?

    Ahora tienes componentes que manejan sus propias referencias al DOM y efectos secundarios — pero los addons reales de Odoo rara vez viven aislados del servidor. El Capítulo 12 cubre orm y rpc, los servicios que permiten que tus componentes lean y escriban datos de negocio reales.

    en Libro OWL
    Capítulo 12: Comunicación con el Servidor usando useService
    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