¿Por qué este capítulo? Más incidentes de producción de los que he atendido se remontan a errores de ciclo de vida que a casi cualquier otra cosa — un fetch que se dispara antes de que exista el DOM, un listener que nunca se limpia y va filtrando memoria lentamente a lo largo de una sesión larga. ¿Qué trata de resolver Odoo con esto? Una vista de backend puede montar y desmontar docenas de widgets mientras un usuario navega — los hooks de ciclo de vida son cómo Odoo garantiza que cada uno se inicialice y se desmonte limpiamente, sin que queden timers o listeners acumulándose. Aplicación en la vida real: Cargar los registros relacionados de un partner antes del primer render (
onWillStart), enfocar un input cuando se abre un formulario (onMounted), y limpiar un intervalo cuando se quita una tarjeta kanban (onWillUnmount) son todos hooks de ciclo de vida haciendo exactamente el trabajo que describe este capítulo.
Un componente, al igual que un organismo vivo, tiene un ciclo de vida. Nace (se crea y se monta en el DOM), vive (se actualiza en respuesta a cambios de estado y props), y eventualmente, muere (se desmonta y se remueve del DOM).
OWL proporciona una forma poderosa de aprovechar estos momentos clave en la vida de un componente usando hooks del ciclo de vida. Estos hooks son funciones especiales que puedes llamar dentro de tu método setup para registrar callbacks que se ejecutarán en puntos específicos del ciclo de vida.
Entender estos hooks es esencial para manejar efectos secundarios, obtener datos correctamente y limpiar recursos para prevenir fugas de memoria. Exploremos los hooks más importantes que usarás en tu desarrollo día a día.
El Ciclo de Vida de un Vistazo
Antes de profundizar en los hooks individuales, visualicemos el viaje completo de un componente:
- Fase de Creación:
setup(): Se ejecuta la lógica de configuración principal del componente y registra los hooks.onWillStart: Se ejecuta antes del primer renderizado, perfecto para obtener datos iniciales.-
La instancia del componente se crea y se prepara.
-
Fase de Montaje (Añadido a la página):
- El componente renderiza su plantilla HTML.
- El HTML se inserta en el DOM.
-
onMounted: El componente ahora está vivo en la página. -
Fase de Actualización (Durante su vida - se repite según sea necesario):
- El padre pasa nuevas props →
onWillUpdateProps. - Los cambios de estado activan re-renderizado.
- El componente re-renderiza su HTML.
-
El DOM se actualiza con los cambios.
-
Fase de Desmontaje (Remoción de la página):
onWillUnmount: El componente está a punto de ser destruido.- El componente es removido del DOM.
- La memoria y recursos se limpian.
{width=100%}
onWillStart: Obteniendo Datos Iniciales
El hook onWillStart es único porque es el único hook que puede ser asíncrono. Se ejecuta antes del renderizado inicial del componente, lo que lo hace el lugar perfecto para obtener datos que el componente necesita para su primera aparición.
- Cuándo se ejecuta: Después de que
setup()termina, pero antes del renderizado inicial. - Característica clave: Puede ser async - el componente espera a que la promesa se resuelva.
- Caso de uso: Obtener datos esenciales que deben estar listos antes del primer renderizado.
Ejemplo Básico: Lista de Productos
Creemos un componente ProductList que obtiene productos antes de renderizar:
JavaScript (product_list.js):
import { Component, onWillStart, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class ProductList extends Component {
static template = "my_module.ProductList";
setup() {
this.state = useState({
products: [],
loading: true,
error: null
});
this.orm = useService("orm");
onWillStart(async () => {
try {
console.log("ProductList: Obteniendo productos antes del primer renderizado...");
const data = await this.orm.searchRead(
"product.product",
[],
["name", "list_price", "categ_id"]
);
this.state.products = data;
console.log(`ProductList: Se cargaron ${data.length} productos`);
} catch (error) {
console.error("Falló la carga de productos:", error);
this.state.error = "Falló la carga de productos";
} finally {
this.state.loading = false;
}
});
}
get formattedProducts() {
return this.state.products.map(product => ({
...product,
formattedPrice: `$${product.list_price.toFixed(2)}`
}));
}
}
Plantilla (product_list.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.ProductList" owl="1">
<div class="product-list">
<!-- Estado de Carga -->
<t t-if="state.loading">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p class="mt-2 text-muted">Cargando productos...</p>
</div>
</t>
<!-- Estado de Error -->
<t t-elif="state.error">
<div class="alert alert-danger" t-esc="state.error"/>
</t>
<!-- Cuadrícula de Productos -->
<t t-else="">
<div class="row">
<t t-foreach="formattedProducts" t-as="product" t-key="product.id">
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title" t-esc="product.name"/>
<p class="card-text">
<strong t-esc="product.formattedPrice"/>
</p>
<small class="text-muted" t-esc="product.categ_id[1]"/>
</div>
</div>
</div>
</t>
</div>
</t>
</div>
</t>
</templates>
Al obtener datos en onWillStart, previenes que el componente renderice un estado vacío y luego "parpadee" cuando llegan los datos. El componente esperará a que la promesa de onWillStart se resuelva antes de realizar su renderizado inicial.
Patrones Avanzados de onWillStart
Múltiples Operaciones Asíncronas:
onWillStart(async () => {
// Ejecutar múltiples operaciones en paralelo
const [products, categories, suppliers] = await Promise.all([
this.orm.searchRead("product.product", [], ["name", "list_price"]),
this.orm.searchRead("product.category", [], ["name"]),
this.orm.searchRead("res.partner", [["supplier_rank", ">", 0]], ["name"])
]);
this.state.products = products;
this.state.categories = categories;
this.state.suppliers = suppliers;
});
Patrón de Recuperación de Errores:
onWillStart(async () => {
try {
// Intentar fuente de datos principal
this.state.data = await this.fetchFromPrimarySource();
} catch (primaryError) {
console.warn("Falló la fuente principal, intentando respaldo:", primaryError);
try {
// Intentar fuente de datos de respaldo
this.state.data = await this.fetchFromFallbackSource();
this.state.usingFallback = true;
} catch (fallbackError) {
console.error("Fallaron todas las fuentes de datos:", fallbackError);
this.state.error = "No se pueden cargar datos de ninguna fuente";
}
}
});
onMounted: Interactuando con el DOM
El hook onMounted se ejecuta después de que el componente ha sido renderizado por primera vez y su HTML ha sido añadido al DOM. Esta es tu puerta de entrada para manipulación del DOM e integración de librerías de terceros.
- Cuándo se ejecuta: Después del renderizado inicial e inserción en el DOM.
- Caso de uso: Cualquier tarea que requiera que el HTML del componente exista en el DOM.
Casos de Uso Clave:
- Enfocar elementos input
- Inicializar librerías de terceros (gráficos, mapas, calendarios)
- Medir dimensiones de elementos
- Configurar event listeners del DOM
Para obtener una referencia a un elemento específico en tu plantilla, usas la directiva t-ref y el hook useRef.
Ejemplo: Componente de Búsqueda con Enfoque Automático
JavaScript (search_component.js):
import { Component, onMounted, useRef, useState } from "@odoo/owl";
export class SearchComponent extends Component {
static template = "my_module.SearchComponent";
setup() {
this.state = useState({
searchTerm: "",
results: []
});
// Crear una referencia al input de búsqueda
this.searchInputRef = useRef("search-input");
onMounted(() => {
console.log("SearchComponent: Componente montado, enfocando input");
// El elemento input ahora está en el DOM y puede ser enfocado
if (this.searchInputRef.el) {
this.searchInputRef.el.focus();
console.log("SearchComponent: Input enfocado exitosamente");
}
});
}
onSearchInput(event) {
this.state.searchTerm = event.target.value;
console.log("Término de búsqueda cambiado:", this.state.searchTerm);
// Aquí podrías activar una llamada a API de búsqueda
}
}
Plantilla (search_component.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.SearchComponent" owl="1">
<div class="search-component">
<div class="search-container mb-3">
<input
type="text"
class="form-control"
t-ref="search-input"
placeholder="Buscar productos..."
t-model="state.searchTerm"
t-on-input="onSearchInput"
/>
</div>
<div class="search-results">
<t t-if="state.searchTerm">
<p class="text-muted">
Buscando: "<t t-esc="state.searchTerm"/>"
</p>
</t>
<!-- Los resultados irían aquí -->
<div class="results-list">
<!-- Implementación de resultados -->
</div>
</div>
</div>
</t>
</templates>
Ejemplo Avanzado de onMounted: Integración de Gráficos
Aquí hay un ejemplo más complejo que se integra con Chart.js:
JavaScript (sales_chart.js):
import { Component, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class SalesChart extends Component {
static template = "my_module.SalesChart";
setup() {
this.state = useState({
chartData: [],
loading: true
});
this.chartCanvasRef = useRef("sales-chart");
this.orm = useService("orm");
this.chartInstance = null;
onMounted(async () => {
console.log("SalesChart: Componente montado, inicializando gráfico");
await this.loadSalesData();
this.initializeChart();
});
onWillUnmount(() => {
// Limpiar el gráfico cuando el componente se destruya
if (this.chartInstance) {
console.log("SalesChart: Destruyendo instancia de gráfico");
this.chartInstance.destroy();
}
});
}
async loadSalesData() {
try {
const salesData = await this.orm.searchRead(
"sale.order",
[["state", "=", "sale"]],
["date_order", "amount_total"]
);
this.state.chartData = salesData;
this.state.loading = false;
} catch (error) {
console.error("Falló la carga de datos de ventas:", error);
this.state.loading = false;
}
}
initializeChart() {
if (!this.chartCanvasRef.el || this.state.loading) {
return;
}
// Asumiendo que Chart.js está cargado globalmente
const ctx = this.chartCanvasRef.el.getContext('2d');
// Procesar datos para el gráfico
const processedData = this.processChartData();
this.chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: processedData.labels,
datasets: [{
label: 'Ingresos por Ventas',
data: processedData.data,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Rendimiento de Ventas'
}
}
}
});
console.log("SalesChart: Gráfico inicializado exitosamente");
}
processChartData() {
// Agrupar ventas por mes y sumar cantidades
const monthlyData = {};
this.state.chartData.forEach(order => {
const month = order.date_order.substring(0, 7); // Formato YYYY-MM
if (!monthlyData[month]) {
monthlyData[month] = 0;
}
monthlyData[month] += order.amount_total;
});
const sortedMonths = Object.keys(monthlyData).sort();
return {
labels: sortedMonths,
data: sortedMonths.map(month => monthlyData[month])
};
}
}
Plantilla (sales_chart.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.SalesChart" owl="1">
<div class="sales-chart">
<t t-if="state.loading">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando datos del gráfico...</span>
</div>
</div>
</t>
<t t-else="">
<div class="chart-container">
<canvas t-ref="sales-chart" width="400" height="200"></canvas>
</div>
</t>
</div>
</t>
</templates>
onWillUpdateProps: Reaccionando a Cambios de Props
Este hook se llama cuando un componente padre está a punto de pasar nuevas props al hijo. Permite que el hijo reaccione a los cambios entrantes antes de que se re-renderice.
- Cuándo se ejecuta: Cuando se reciben nuevas props, antes de que el componente se actualice.
- Caso de uso: Cuando un componente hijo necesita obtener nuevos datos basados en props cambiadas.
Ejemplo: Componente de Perfil de Usuario
Imagina un componente UserProfile que muestra detalles del usuario. Cuando el padre cambia la prop userId, el componente necesita obtener los datos del nuevo usuario:
JavaScript (user_profile.js):
import { Component, onWillStart, onWillUpdateProps, 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: true,
error: null
});
this.orm = useService("orm");
// Cargar datos iniciales del usuario
onWillStart(async () => {
await this.loadUser(this.props.userId);
});
// Reaccionar a cambios de props
onWillUpdateProps(async (nextProps) => {
console.log("UserProfile: Las props se van a actualizar", {
current: this.props.userId,
next: nextProps.userId
});
// Solo recargar si el ID del usuario realmente cambió
if (this.props.userId !== nextProps.userId) {
console.log(`UserProfile: ID de usuario cambió de ${this.props.userId} a ${nextProps.userId}`);
this.state.loading = true;
await this.loadUser(nextProps.userId);
}
});
}
async loadUser(userId) {
try {
console.log(`UserProfile: Cargando datos de usuario para ID ${userId}`);
const users = await this.orm.read("res.users", [userId], [
"name",
"email",
"phone",
"partner_id"
]);
if (users.length > 0) {
this.state.user = users[0];
this.state.error = null;
console.log(`UserProfile: Usuario ${users[0].name} cargado exitosamente`);
} else {
throw new Error(`Usuario con ID ${userId} no encontrado`);
}
} catch (error) {
console.error(`Falló la carga del usuario ${userId}:`, error);
this.state.error = `Falló la carga del usuario: ${error.message}`;
this.state.user = null;
} finally {
this.state.loading = false;
}
}
}
Plantilla (user_profile.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.UserProfile" owl="1">
<div class="user-profile card">
<div class="card-header">
<h5>Perfil de Usuario</h5>
</div>
<div class="card-body">
<!-- Estado de Carga -->
<t t-if="state.loading">
<div class="text-center py-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando usuario...</span>
</div>
<p class="mt-2 text-muted">Cargando perfil de usuario...</p>
</div>
</t>
<!-- Estado de Error -->
<t t-elif="state.error">
<div class="alert alert-danger" t-esc="state.error"/>
</t>
<!-- Datos del Usuario -->
<t t-elif="state.user">
<div class="user-info">
<h6 class="mb-3" t-esc="state.user.name"/>
<div class="user-details">
<div class="mb-2">
<strong>Email:</strong>
<span t-esc="state.user.email"/>
</div>
<t t-if="state.user.phone">
<div class="mb-2">
<strong>Teléfono:</strong>
<span t-esc="state.user.phone"/>
</div>
</t>
<div class="mb-2">
<strong>ID de Partner:</strong>
<span t-esc="state.user.partner_id[1]"/>
</div>
</div>
</div>
</t>
</div>
</div>
</t>
</templates>
Ejemplo de Componente Padre
Aquí hay un ejemplo de cómo un componente padre podría usar el UserProfile:
JavaScript (user_manager.js):
import { Component, useState } from "@odoo/owl";
import { UserProfile } from "../user_profile/user_profile";
export class UserManager extends Component {
static template = "my_module.UserManager";
static components = { UserProfile };
setup() {
this.state = useState({
selectedUserId: 1,
availableUsers: [
{ id: 1, name: "Alice Johnson" },
{ id: 2, name: "Bob Smith" },
{ id: 3, name: "Carol Brown" }
]
});
}
selectUser(userId) {
console.log("UserManager: Seleccionando usuario", userId);
this.state.selectedUserId = userId;
}
}
Plantilla (user_manager.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.UserManager" owl="1">
<div class="user-manager">
<div class="row">
<!-- Selección de Usuario -->
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h6>Seleccionar Usuario</h6>
</div>
<div class="card-body">
<t t-foreach="state.availableUsers" t-as="user" t-key="user.id">
<button
class="btn btn-outline-primary btn-sm me-2 mb-2"
t-att-class="state.selectedUserId === user.id ? 'btn btn-primary btn-sm me-2 mb-2' : 'btn btn-outline-primary btn-sm me-2 mb-2'"
t-on-click="() => this.selectUser(user.id)">
<t t-esc="user.name"/>
</button>
</t>
</div>
</div>
</div>
<!-- Mostrar Perfil de Usuario -->
<div class="col-md-8">
<UserProfile userId="state.selectedUserId"/>
</div>
</div>
</div>
</t>
</templates>
Este patrón asegura que el componente UserProfile siempre muestre datos consistentes con la prop userId que recibe de su padre. Cuando el usuario hace clic en un botón de usuario diferente en el padre, el UserProfile automáticamente detectará el cambio de prop y obtendrá los datos del nuevo usuario.
onWillUnmount: Limpieza
El hook onWillUnmount es el último aliento del componente. Se ejecuta justo antes de que el componente sea removido del DOM. Esto es crucial para la limpieza y prevenir fugas de memoria.
- Cuándo se ejecuta: Justo antes de que el componente sea destruido.
- Caso de uso crítico: Limpiar recursos que de otra manera persistirían después de la destrucción del componente.
Qué Limpiar:
- Timers e intervalos (
setInterval,setTimeout) - Event listeners globales (en
window,document) - Instancias de librerías de terceros (gráficos, mapas, etc.)
- Conexiones WebSocket
- Peticiones API en curso (si son cancelables)
Ejemplo: Componente Basado en Timer
JavaScript (live_clock.js):
import { Component, onMounted, onWillUnmount, useState } from "@odoo/owl";
export class LiveClock extends Component {
static template = "my_module.LiveClock";
setup() {
this.state = useState({
currentTime: new Date().toLocaleTimeString(),
isRunning: true
});
// Almacenar referencia del timer para limpieza
this.timer = null;
onMounted(() => {
console.log("LiveClock: Iniciando timer del reloj");
this.startClock();
});
onWillUnmount(() => {
console.log("LiveClock: Limpiando timer del reloj");
this.stopClock();
});
}
startClock() {
// Actualizar tiempo cada segundo
this.timer = setInterval(() => {
if (this.state.isRunning) {
this.state.currentTime = new Date().toLocaleTimeString();
}
}, 1000);
}
stopClock() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
console.log("LiveClock: Timer limpiado exitosamente");
}
}
toggleClock() {
this.state.isRunning = !this.state.isRunning;
console.log("LiveClock: Reloj", this.state.isRunning ? "reanudado" : "pausado");
}
}
Plantilla (live_clock.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.LiveClock" owl="1">
<div class="live-clock card">
<div class="card-body text-center">
<h3 class="display-4 mb-3" t-esc="state.currentTime"/>
<button
class="btn btn-primary"
t-on-click="toggleClock">
<t t-esc="state.isRunning ? 'Pausar' : 'Reanudar'"/>
</button>
</div>
</div>
</t>
</templates>
Ejemplo Avanzado de Limpieza: Múltiples Recursos
JavaScript (notification_system.js):
import { Component, onMounted, onWillUnmount, useState } from "@odoo/owl";
export class NotificationSystem extends Component {
static template = "my_module.NotificationSystem";
setup() {
this.state = useState({
notifications: [],
isConnected: false
});
// Almacenar referencias de limpieza
this.cleanupTasks = [];
onMounted(() => {
this.initializeSystem();
});
onWillUnmount(() => {
console.log("NotificationSystem: Realizando limpieza completa");
this.performCleanup();
});
}
initializeSystem() {
// 1. Configurar chequeo periódico de salud
const healthCheckTimer = setInterval(() => {
this.checkSystemHealth();
}, 5000);
this.cleanupTasks.push(() => {
clearInterval(healthCheckTimer);
console.log("NotificationSystem: Timer de chequeo de salud limpiado");
});
// 2. Configurar listeners de focus/blur de ventana
const handleWindowFocus = () => {
console.log("NotificationSystem: Ventana enfocada, refrescando notificaciones");
this.refreshNotifications();
};
const handleWindowBlur = () => {
console.log("NotificationSystem: Ventana desenfocada");
};
window.addEventListener('focus', handleWindowFocus);
window.addEventListener('blur', handleWindowBlur);
this.cleanupTasks.push(() => {
window.removeEventListener('focus', handleWindowFocus);
window.removeEventListener('blur', handleWindowBlur);
console.log("NotificationSystem: Event listeners de ventana removidos");
});
// 3. Configurar listeners de online/offline
const handleOnline = () => {
this.state.isConnected = true;
this.refreshNotifications();
};
const handleOffline = () => {
this.state.isConnected = false;
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
this.cleanupTasks.push(() => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
console.log("NotificationSystem: Event listeners de red removidos");
});
// 4. Inicializar estado de conexión
this.state.isConnected = navigator.onLine;
}
Fíjate en el patrón: cada vez que initializeSystem configura algo (un timer, un par de listeners), inmediatamente agrega una función "deshacer" correspondiente a this.cleanupTasks. Eso es lo que permite que la única llamada a performCleanup() de abajo pueda revertir todo, sin importar cuántos recursos se hayan iniciado.
checkSystemHealth() {
// Simular chequeo de salud del sistema
console.log("NotificationSystem: Realizando chequeo de salud");
}
refreshNotifications() {
// Simular refresco de notificaciones
console.log("NotificationSystem: Refrescando notificaciones");
}
performCleanup() {
// Ejecutar todas las tareas de limpieza
this.cleanupTasks.forEach((cleanup, index) => {
try {
cleanup();
} catch (error) {
console.error(`NotificationSystem: Tarea de limpieza ${index} falló:`, error);
}
});
// Limpiar el array de tareas de limpieza
this.cleanupTasks = [];
console.log("NotificationSystem: Todas las tareas de limpieza completadas");
}
}
Si olvidas limpiar en onWillUnmount, recursos como timers seguirían ejecutándose en el fondo para siempre, incluso después de que el componente haya desaparecido, creando fugas de memoria. Usar onWillUnmount para limpieza es una parte crítica de escribir aplicaciones robustas.
Mejores Prácticas del Ciclo de Vida
QUÉ HACER
- Siempre limpiar en onWillUnmount - Limpiar timers, remover event listeners, destruir instancias de terceros
- Usar onWillStart para datos críticos - Datos que deben estar listos antes del primer renderizado
- Hacer onWillStart asíncrono - Aprovechar su capacidad asíncrona única
- Manejar errores graciosamente - Envolver operaciones asíncronas en bloques try-catch
- Loggear eventos del ciclo de vida - Ayuda con debugging durante desarrollo
QUÉ NO HACER
- Nunca mutar props - Las props son de solo lectura en todos los hooks del ciclo de vida
- No olvidar la limpieza - Las fugas de memoria dañarán el rendimiento de tu aplicación
- No asumir que el DOM existe - Solo
onMountedy hooks posteriores tienen acceso al DOM - No realizar cómputos pesados - Mantén los hooks del ciclo de vida rápidos y enfocados
Patrones Comunes
Patrón 1: Carga Condicional de Datos
onWillUpdateProps(async (nextProps) => {
// Solo obtener si la prop importante cambió
if (this.props.customerId !== nextProps.customerId) {
await this.loadCustomerData(nextProps.customerId);
}
});
Patrón 2: Helper de Limpieza
setup() {
this.cleanupFunctions = [];
onMounted(() => {
// Configurar algo que necesita limpieza
const timer = setInterval(this.updateData, 1000);
this.cleanupFunctions.push(() => clearInterval(timer));
});
onWillUnmount(() => {
this.cleanupFunctions.forEach(cleanup => cleanup());
});
}
Patrón 3: Límite de Error
onWillStart(async () => {
try {
await this.loadCriticalData();
} catch (error) {
console.error("Falló la carga de datos críticos:", error);
this.state.error = "Sistema temporalmente no disponible";
// También podría despachar al padre o mostrar UI de respaldo
}
});
Debuggeando Problemas del Ciclo de Vida
Al trabajar con hooks del ciclo de vida, los problemas comunes incluyen:
Problema: El componente no se actualiza cuando cambian las props
Solución: Añadir manejador de onWillUpdateProps:
onWillUpdateProps(async (nextProps) => {
if (this.props.dataId !== nextProps.dataId) {
await this.loadNewData(nextProps.dataId);
}
});
Problema: Fugas de memoria por timers olvidados
Solución: Siempre limpiar en onWillUnmount:
onMounted(() => {
this.timer = setInterval(this.doSomething, 1000);
});
onWillUnmount(() => {
if (this.timer) {
clearInterval(this.timer);
}
});
Problema: El componente renderiza vacío y luego parpadea cuando los datos cargan
Solución: Usar onWillStart en lugar de onMounted:
// Esto causa parpadeo
onMounted(async () => {
this.state.data = await this.fetchData();
});
// Esto previene el parpadeo
onWillStart(async () => {
this.state.data = await this.fetchData();
});
Manejo de Errores con onError y Error Boundaries
Todo lo anterior asume que el renderizado sale bien. No siempre es así. Si un componente hijo lanza un error mientras renderiza —una referencia null, una respuesta de API con forma inesperada, lo que sea— el comportamiento por defecto de OWL es destruir toda la aplicación, no solo el componente que falló. Un bug en un widget pequeño puede tumbar toda la página.
Un error boundary (límite de errores) es un componente que captura los errores lanzados en cualquier parte de su subárbol y muestra una interfaz de reemplazo en vez de dejar que el fallo se propague. OWL provee el hook onError exactamente para esto.
onError(callback) registra una función que OWL llama cada vez que ocurre un error de renderizado o de ciclo de vida en algún descendiente de este componente. No captura errores lanzados dentro de manejadores de eventos (un manejador t-on-click que lanza un error es territorio de tu propio try/catch) — solo errores del mecanismo de renderizado/ciclo de vida.
import { Component, useState, onError, xml } from "@odoo/owl";
class ErrorBoundary extends Component {
static template = xml`
<t t-if="state.error" t-slot="fallback">
<div class="alert alert-danger">Algo salió mal.</div>
</t>
<t t-else="" t-slot="default"/>`;
setup() {
this.state = useState({ error: false });
onError(() => {
this.state.error = true;
});
}
}
Envuelve cualquier parte del árbol en la que no confíes del todo (un widget que renderiza contenido generado por el usuario, una integración de terceros) con este componente, pasando el contenido riesgoso como su slot por defecto y opcionalmente un slot fallback con un mensaje personalizado. Si tu propio manejador onError no puede recuperarse, puede relanzar el error para que un boundary más arriba en el árbol tenga la oportunidad de manejarlo. Eso sí, asegúrate de que la plantilla de fallback nunca lance un error—eso crearía un bucle infinito de fallos.
Otros Hooks de Ciclo de Vida
onWillStart, onMounted, onWillUpdateProps y onWillUnmount cubren los hooks que usarás a diario. OWL expone algunos más para necesidades menos comunes y más avanzadas—vale la pena saber que existen para reconocerlos en código que no escribiste tú:
onWillRender()— se ejecuta justo antes de que la función de plantilla de un componente se ejecute (padres antes que hijos). Rara vez se necesita directamente.onRendered()— se ejecuta justo después de que la función de plantilla se ejecuta, pero antes de que el resultado se aplique al DOM real. También se necesita rara vez directamente.onWillPatch()— se ejecuta justo antes de que se actualice el DOM de un componente ya montado (padres antes que hijos). Útil para leer el estado del DOM —como una posición de scroll— antes de que cambie.onPatched()— se ejecuta justo después de que se actualizó el DOM de un componente ya montado (hijos antes que padres). Este sí lo usarás alguna vez: por ejemplo, para volver a medir el tamaño de un elemento después de que su contenido cambió.javascript onPatched(() => { // el DOM acaba de actualizarse con el estado/props más recientes this.scrollToBottom(); });onWillDestroy()— siempre se llama cuando un componente está siendo destruido, incluso si nunca terminó de montarse. Úsalo para limpieza que debe ocurrir pase lo que pase (a diferencia deonWillUnmount, que solo se dispara para componentes que realmente llegaron a montarse).
Para un desarrollador junior, la regla práctica es: recurre primero a los cuatro hooks vistos antes en este capítulo, y solo mira este segundo grupo cuando tengas un problema específico relacionado con actualizaciones del DOM.
Errores Comunes
Error 1: Cargar Datos en onMounted en Vez de onWillStart
Cargar datos en onMounted hace que el componente se renderice vacío primero y luego parpadee cuando llegan los datos. Usa onWillStart para los datos que el componente necesita para su primer render.
Error 2: Olvidar la Limpieza en onWillUnmount
Los timers, listeners en window/document e instancias de librerías de terceros iniciados en onMounted siguen ejecutándose después de que el componente se destruye, a menos que los limpies en onWillUnmount — una fuente clásica de fugas de memoria.
Error 3: Asumir que el DOM Existe Demasiado Pronto
this.miRef.el solo está garantizado a partir de onMounted. Acceder a un useRef dentro de setup() u onWillStart te dará undefined.
Error 4: Recargar Datos en Cada Llamada a onWillUpdateProps
onWillUpdateProps se ejecuta cuando cualquier prop cambia, no solo la que te interesa. Compara siempre el valor anterior y el nuevo (this.props.x !== nextProps.x) antes de volver a cargar datos.
Error 5: Olvidar un Error Boundary en Algún Punto del Árbol
Sin un manejador onError en algún lugar por encima de él, un solo componente hijo que lanza un error tumba toda la aplicación, no solo a sí mismo. Envuelve los subárboles riesgosos (widgets de terceros, cualquier cosa que renderice datos impredecibles) en un componente ErrorBoundary.
Ejercicios
Ejercicio 1: Corrige el Parpadeo
Toma un componente que carga sus datos dentro de onMounted y mueve la carga a onWillStart. Confirma en el navegador que el parpadeo de "cargando" desaparece en el primer render.
Ejercicio 2: Tapa la Fuga
Escribe un componente que inicie un setInterval en onMounted sin limpiarlo. Abre la consola, monta y desmonta el componente varias veces, y observa que el timer sigue disparándose. Luego corrígelo con onWillUnmount.
Ejercicio 3: Recarga Selectiva
Construye un componente con dos props, userId y theme. En onWillUpdateProps, vuelve a cargar los datos del usuario solo cuando userId cambia — no cuando cambia theme. Añade llamadas a console.log para probar que la carga se omite en actualizaciones que solo cambian el tema.
Ejercicio 4: Construye un Error Boundary
Escribe un componente Buggy cuyo setup() lance un error si una prop crash es true. Envuélvelo en el componente ErrorBoundary de este capítulo y confirma que, cuando crash es true, el resto de la página sigue funcionando y muestra el mensaje de fallback en vez de una pantalla en blanco.
Resumen
Los hooks del ciclo de vida son la base de componentes OWL robustos. Te permiten:
onWillStart: Obtener datos críticos antes del renderizadoonMounted: Interactuar con el DOM e inicializar librerías de tercerosonWillUpdateProps: Reaccionar a cambios de props y obtener nuevos datosonWillUnmount: Limpiar recursos para prevenir fugas de memoria
Domina estos hooks, y podrás crear componentes que no solo son funcionales, sino también performantes y confiables.
TL;DR: onWillStart carga datos antes del primer render, onMounted toca el DOM, onWillUpdateProps reacciona a nuevas props, y onWillUnmount limpia — te saltas el último y filtras memoria.
Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch10_ex1. Las instrucciones de instalación están en el README del repositorio.
¿Qué Sigue?
En el próximo capítulo, exploraremos el poder de los hooks personalizados y cómo pueden ayudarte a crear patrones de ciclo de vida reutilizables a través de tu aplicación.