Effects

Intermedio
Angular
Angular
Actualizado: 25/09/2025

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 - Autor del tutorial

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.