¿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 enonWillUnmount, 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 — yuseEffect/useRefdan 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 comothis.timer. En OWL,useRefes 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";useEffectes 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)— mezclanuevosValoresen el entorno para este componente y todos sus hijos.useChildSubEnv(nuevosValores)— mezclanuevosValoresen el entorno solo para los hijos, dejando elenvpropio 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
- Agrupar lógica relacionada - Poner configuración y limpieza para la misma característica en un efecto
- Usar múltiples efectos - Separar responsabilidades en diferentes efectos
- Incluir todas las dependencias - La función de dependencias debe devolver todos los valores reactivos que el efecto usa
- Retornar funciones de limpieza - Prevenir memory leaks limpiando recursos
- 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,useRefsolo para elementos DOM
Qué No Hacer
- No pasar un array plano como dependencias - OWL espera una función que devuelve un array (
() => [..]), no el array en sí - No usar efectos para todo - Alguna lógica pertenece a event handlers, no a efectos
- No reemplazar
onWillStartcon un efecto - Los efectos se ejecutan después del montaje; soloonWillStartpuede retrasar el primer renderizado - 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:
useRefcrea referencias estables a elementos DOM (y las propiedades de instancia cubren cualquier otro valor mutable)useEffectmaneja 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
- Enfocar al montar: Construye un componente pequeño con un input de texto y un
useRef. UsauseEffectcon una función de dependencias vacía (() => []) para enfocar el input tan pronto como el componente se monte. - Corrige el bug de dependencias: Toma este fragmento roto y corrígelo para que el efecto realmente se re-ejecute cuando
state.querycambie:javascript useEffect(() => { console.log("Buscando", this.state.query); }, [this.state.query]); - Contador con debounce: Agrega un
useEffectque registre"¡Estable!"en la consola solo después de questate.countno haya cambiado durante 500ms (pista:setTimeouten el efecto,clearTimeouten 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.