Función effect() y side effects
Los signals que hemos visto hasta ahora nos permiten manejar estado reactivo de forma declarativa, pero a veces necesitamos ejecutar código cuando ese estado cambia. Para esto Angular proporciona la función effect()
, que nos permite realizar side effects de forma reactiva.
Qué son los side effects
Un side effect es cualquier operación que tiene un impacto fuera del ámbito del cálculo actual. A diferencia de los computed signals, que están diseñados para derivar nuevos valores, los effects están pensados para ejecutar código que interactúa con el mundo exterior.
Ejemplos típicos de side effects incluyen:
- Logging de cambios de estado
- Manipulación del DOM directamente
- Sincronización con localStorage o sessionStorage
- Llamadas a APIs externas
- Actualización de elementos fuera del template
Sintaxis básica de effect()
La función effect()
acepta una función que se ejecutará automáticamente cada vez que cambien los signals que lee en su interior:
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-example',
standalone: true,
template: `
<p>Contador: {{ contador() }}</p>
<button (click)="incrementar()">+</button>
`
})
export class ExampleComponent {
contador = signal(0);
constructor() {
// Effect que se ejecuta cuando cambia contador
effect(() => {
console.log('El contador cambió a:', this.contador());
});
}
incrementar() {
this.contador.update(value => value + 1);
}
}
Detección automática de dependencias
Los effects rastrean automáticamente qué signals leen durante su ejecución. No necesitas declarar dependencias explícitamente como en otros frameworks:
export class UserProfileComponent {
userName = signal('');
userAge = signal(0);
theme = signal('light');
constructor() {
// Este effect depende de userName y userAge automáticamente
effect(() => {
const name = this.userName();
const age = this.userAge();
console.log(`Usuario: ${name}, Edad: ${age}`);
// theme no se lee aquí, por lo que no es una dependencia
});
// Effect independiente que solo depende de theme
effect(() => {
document.body.className = this.theme();
});
}
}
Injection Context y ubicación
Los effects deben crearse dentro de un injection context, típicamente en el constructor del componente, servicios o durante la inicialización:
export class DataService {
private data = signal<any[]>([]);
constructor() {
// ✅ Correcto: dentro del constructor
effect(() => {
localStorage.setItem('app-data', JSON.stringify(this.data()));
});
}
// ❌ Incorrecto: fuera del injection context
someMethod() {
effect(() => {
// Esto fallará
});
}
}
Ejemplos prácticos de side effects
Ejemplo 1: Sincronización con localStorage
@Component({
selector: 'app-preferences',
standalone: true,
template: `
<label>
<input
type="checkbox"
[checked]="darkMode()"
(change)="toggleDarkMode()">
Modo oscuro
</label>
`
})
export class PreferencesComponent {
darkMode = signal(false);
constructor() {
// Cargar preferencia desde localStorage
const saved = localStorage.getItem('darkMode');
if (saved) {
this.darkMode.set(JSON.parse(saved));
}
// Guardar cambios automáticamente
effect(() => {
localStorage.setItem('darkMode', JSON.stringify(this.darkMode()));
});
}
toggleDarkMode() {
this.darkMode.update(current => !current);
}
}
Ejemplo 2: Logging de cambios
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Contador: {{ value() }}</p>
<button (click)="increment()">Incrementar</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
value = signal(0);
constructor() {
// Log cada cambio con timestamp
effect(() => {
const current = this.value();
console.log(`[${new Date().toISOString()}] Contador: ${current}`);
});
}
increment() {
this.value.update(v => v + 1);
}
reset() {
this.value.set(0);
}
}
Ejemplo 3: Actualización del título de página
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>{{ pageTitle() }}</h1>
<button (click)="changePage('home')">Home</button>
<button (click)="changePage('about')">About</button>
`
})
export class AppComponent {
pageTitle = signal('Mi Aplicación');
constructor() {
// Actualizar título del navegador automáticamente
effect(() => {
document.title = this.pageTitle();
});
}
changePage(page: string) {
const titles = {
home: 'Inicio - Mi Aplicación',
about: 'Acerca de - Mi Aplicación'
};
this.pageTitle.set(titles[page] || 'Mi Aplicación');
}
}
Effects vs Computed Signals
Es importante entender la diferencia fundamental entre effects y computed signals:
- Computed signals: Para derivar nuevos valores de forma reactiva
- Effects: Para ejecutar side effects cuando cambian los signals
export class ProductComponent {
quantity = signal(1);
price = signal(100);
// ✅ Computed: deriva un nuevo valor
total = computed(() => this.quantity() * this.price());
constructor() {
// ✅ Effect: side effect (logging)
effect(() => {
console.log('Total actualizado:', this.total());
});
// ❌ Incorrecto: no uses effects para derivar valores
// effect(() => {
// this.someOtherSignal.set(this.quantity() * this.price());
// });
}
}
Los effects se ejecutan de forma asíncrona después de que Angular procese todos los cambios de signals, garantizando que el estado esté estabilizado antes de ejecutar los side effects. Esto los hace ideales para operaciones que no afectan directamente al renderizado pero que necesitan reaccionar a cambios de estado.
Cleanup y lifecycle effects
Los effects en Angular tienen un ciclo de vida bien definido y proporcionan mecanismos para limpiar recursos cuando ya no son necesarios. Esta gestión es crucial para evitar memory leaks y comportamientos inesperados en aplicaciones complejas.
Ciclo de vida automático de effects
Por defecto, los effects se destruyen automáticamente cuando el componente o servicio que los contiene es destruido. Angular se encarga de esta limpieza sin intervención manual:
@Component({
selector: 'app-timer',
standalone: true,
template: `<p>Tiempo: {{ tiempo() }}s</p>`
})
export class TimerComponent implements OnDestroy {
tiempo = signal(0);
constructor() {
// Este effect se limpia automáticamente al destruir el componente
effect(() => {
console.log('Tiempo actual:', this.tiempo());
});
}
ngOnDestroy() {
// No necesitas limpiar el effect manualmente
console.log('Componente destruido');
}
}
Cleanup manual con EffectRef
Cuando necesitas control manual sobre la vida de un effect, puedes usar el EffectRef
que devuelve la función effect()
:
import { Component, effect, signal, EffectRef } from '@angular/core';
@Component({
selector: 'app-conditional-effect',
standalone: true,
template: `
<button (click)="toggleEffect()">
{{ effectActive ? 'Desactivar' : 'Activar' }} Effect
</button>
<p>Contador: {{ contador() }}</p>
<button (click)="incrementar()">+</button>
`
})
export class ConditionalEffectComponent {
contador = signal(0);
effectActive = false;
private logEffect?: EffectRef;
toggleEffect() {
if (this.effectActive) {
// Destruir effect manualmente
this.logEffect?.destroy();
this.logEffect = undefined;
this.effectActive = false;
} else {
// Crear nuevo effect
this.logEffect = effect(() => {
console.log('Contador:', this.contador());
});
this.effectActive = true;
}
}
incrementar() {
this.contador.update(v => v + 1);
}
}
Cleanup de recursos en effects
Los effects que crean subscripciones o recursos externos necesitan limpiarlos adecuadamente. Angular proporciona una función de cleanup que se ejecuta antes de cada re-ejecución y al destruir el effect:
@Component({
selector: 'app-websocket',
standalone: true,
template: `
<p>Estado: {{ connectionStatus() }}</p>
<p>Último mensaje: {{ lastMessage() }}</p>
`
})
export class WebSocketComponent {
connectionStatus = signal<'connecting' | 'connected' | 'disconnected'>('disconnected');
lastMessage = signal<string>('');
constructor() {
effect((onCleanup) => {
const status = this.connectionStatus();
if (status === 'connecting') {
// Simular conexión WebSocket
const ws = new WebSocket('wss://example.com/socket');
ws.onopen = () => {
this.connectionStatus.set('connected');
};
ws.onmessage = (event) => {
this.lastMessage.set(event.data);
};
ws.onerror = () => {
this.connectionStatus.set('disconnected');
};
// Cleanup: cerrar conexión cuando el effect se destruya o re-ejecute
onCleanup(() => {
ws.close();
console.log('WebSocket cerrado');
});
}
});
}
connect() {
this.connectionStatus.set('connecting');
}
disconnect() {
this.connectionStatus.set('disconnected');
}
}
Effects con intervals y timeouts
Los timers son un caso común que requiere cleanup para evitar que continúen ejecutándose después de destruir el componente:
@Component({
selector: 'app-auto-refresh',
standalone: true,
template: `
<p>Datos actualizados: {{ lastUpdate() }}</p>
<p>Auto-refresh: {{ autoRefresh() ? 'Activo' : 'Inactivo' }}</p>
<button (click)="toggleAutoRefresh()">
{{ autoRefresh() ? 'Desactivar' : 'Activar' }} Auto-refresh
</button>
`
})
export class AutoRefreshComponent {
autoRefresh = signal(false);
lastUpdate = signal(new Date().toLocaleTimeString());
constructor() {
effect((onCleanup) => {
if (this.autoRefresh()) {
// Crear interval para actualización automática
const intervalId = setInterval(() => {
this.lastUpdate.set(new Date().toLocaleTimeString());
}, 2000);
// Cleanup: limpiar interval
onCleanup(() => {
clearInterval(intervalId);
console.log('Interval limpiado');
});
}
});
}
toggleAutoRefresh() {
this.autoRefresh.update(current => !current);
}
}
Event listeners y DOM manipulation
Cuando los effects manipulan el DOM directamente o añaden event listeners, es esencial limpiar estos recursos:
@Component({
selector: 'app-keyboard-listener',
standalone: true,
template: `
<p>Escucha de teclado: {{ listening() ? 'Activa' : 'Inactiva' }}</p>
<p>Última tecla: {{ lastKey() }}</p>
<button (click)="toggleListening()">
{{ listening() ? 'Desactivar' : 'Activar' }} Escucha
</button>
`
})
export class KeyboardListenerComponent {
listening = signal(false);
lastKey = signal<string>('');
constructor() {
effect((onCleanup) => {
if (this.listening()) {
const handleKeyPress = (event: KeyboardEvent) => {
this.lastKey.set(event.key);
};
// Añadir event listener
document.addEventListener('keydown', handleKeyPress);
// Cleanup: remover event listener
onCleanup(() => {
document.removeEventListener('keydown', handleKeyPress);
console.log('Event listener removido');
});
}
});
}
toggleListening() {
this.listening.update(current => !current);
}
}
Cleanup condicional y dependencias
Los effects pueden tener cleanup condicional basado en sus dependencias. La función de cleanup se ejecuta antes de cada re-ejecución:
@Component({
selector: 'app-theme-manager',
standalone: true,
template: `
<select (change)="changeTheme($event)">
<option value="light">Claro</option>
<option value="dark">Oscuro</option>
<option value="auto">Automático</option>
</select>
<p>Tema actual: {{ currentTheme() }}</p>
`
})
export class ThemeManagerComponent {
selectedTheme = signal<'light' | 'dark' | 'auto'>('light');
currentTheme = signal<'light' | 'dark'>('light');
constructor() {
effect((onCleanup) => {
const theme = this.selectedTheme();
if (theme === 'auto') {
// Escuchar cambios del sistema
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateTheme = (e: MediaQueryListEvent) => {
this.currentTheme.set(e.matches ? 'dark' : 'light');
};
// Establecer tema inicial
this.currentTheme.set(mediaQuery.matches ? 'dark' : 'light');
// Escuchar cambios
mediaQuery.addEventListener('change', updateTheme);
// Cleanup: remover listener solo si era 'auto'
onCleanup(() => {
mediaQuery.removeEventListener('change', updateTheme);
});
} else {
// Tema manual: aplicar directamente
this.currentTheme.set(theme);
}
});
}
changeTheme(event: Event) {
const select = event.target as HTMLSelectElement;
this.selectedTheme.set(select.value as any);
}
}
Mejores prácticas para cleanup
- Siempre limpia recursos externos: timers, subscripciones, event listeners
- Usa onCleanup para cualquier operación que necesite reversión
- Aprovecha la limpieza automática cuando sea posible
- Evita effects complejos: divide en effects más pequeños si es necesario
El sistema de cleanup en Angular effects garantiza que los recursos se liberen correctamente, manteniendo la aplicación eficiente y libre de memory leaks. La función onCleanup
se ejecuta de forma predecible, proporcionando un punto claro para la limpieza de recursos.

Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Angular
Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender qué son los side effects y su diferencia con los computed signals.
- Aprender a usar la función effect() para ejecutar código reactivo ante cambios en signals.
- Conocer la detección automática de dependencias en effects y su contexto de inyección.
- Entender el ciclo de vida de los effects y cómo realizar limpieza manual o automática de recursos.
- Aplicar buenas prácticas para evitar memory leaks y gestionar recursos externos en effects.