Effects en Angular

Experto
Angular
Angular
Actualizado: 27/08/2024

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

¿Qué son los Effects?

Los Effects en Angular son una característica introducida en la versión 16 que permite ejecutar código de forma reactiva en respuesta a cambios en signals u otras fuentes de datos observables. Forman parte del nuevo sistema de reactividad de Angular y están diseñados para manejar efectos secundarios de manera declarativa y eficiente.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

Un Effect es una función que se ejecuta automáticamente cuando sus dependencias cambian, pero a diferencia de los computeds, no produce un nuevo valor. En su lugar, realiza operaciones que tienen efectos secundarios, como actualizar el DOM, realizar llamadas a APIs, o interactuar con servicios del navegador.

Una característica importante de los Effects es que se ejecutan de forma asíncrona durante el proceso de detección de cambios. Esto significa que Angular agrupa múltiples cambios y ejecuta los efectos en el próximo microtask, lo que puede mejorar el rendimiento al evitar ejecuciones innecesarias.

Crear y usar un Effect

Los Effects se definen utilizando la función effect() proporcionada por Angular. Esta función toma como argumento una función de efecto que contiene el código a ejecutar. El sistema de reactividad de Angular rastrea automáticamente las señales y observables utilizados dentro de la función de efecto, y vuelve a ejecutar el efecto cuando cualquiera de estas dependencias cambia.

Para crear y usar un Effect en Angular, primero debemos importar la función effect del paquete @angular/core. Luego, podemos definir un Effect dentro de un componente o servicio de la siguiente manera:

import { Component, effect, signal } from '@angular/core';

@Component({
  selector: 'app-example',
  template: '<p>{{ message() }}</p>'
})
export class ExampleComponent {
  message = signal('Hola');

  constructor() {
    effect(() => {
      console.log(`El mensaje ha cambiado a: ${this.message()}`);
    });
  }

  updateMessage() {
    this.message.set('Hola Mundo');
  }
}

En este ejemplo, hemos creado un Effect que se ejecutará cada vez que el valor de message cambie. El Effect simplemente registra el nuevo valor en la consola.

Los Effects se ejecutan automáticamente una vez cuando se crean, y luego cada vez que cualquiera de sus dependencias (en este caso, message) cambia. Es importante notar que los Effects no devuelven un valor, sino que realizan acciones secundarias.

Podemos crear Effects más complejos que dependan de múltiples señales o que realicen operaciones más elaboradas:

import { Component, effect, signal, inject } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-user',
  standalone: true,
  imports: [JsonPipe],
  template: '<p>{{ userData() | json }}</p>'
})
export class UserComponent {
  userId = signal(1);
  userData = signal<any>(null);
  private http = inject(HttpClient);

  constructor() {
    effect(() => {
      this.http.get(`https://jsonplaceholder.typicode.com/users/${this.userId()}`)
        .subscribe(data => this.userData.set(data));
    });
  }

  updateUser(id: number) {
    this.userId.set(id);
  }
}

En este caso, el Effect se encarga de realizar una petición HTTP cada vez que userId cambia, actualizando userData con la respuesta.

También es posible pasar opciones al crear un Effect. Por ejemplo, podemos usar la opción allowSignalWrites para permitir que el Effect modifique señales:

effect(() => {
  const newValue = someComputation(this.inputSignal());
  this.outputSignal.set(newValue);
}, { allowSignalWrites: true });

Es importante tener en cuenta que los Effects se ejecutan de forma asíncrona por defecto. Si necesitamos que un Effect se ejecute de forma síncrona, podemos usar la opción manualCleanup:

effect(() => {
  // Este efecto se ejecutará de forma síncrona
  document.title = `User: ${this.userName()}`;
}, { manualCleanup: true });

Finalmente, los Effects se destruyen automáticamente cuando el componente o servicio que los contiene es destruido. Sin embargo, si necesitamos destruir un Effect manualmente, podemos hacerlo de la siguiente manera:

const cleanup = effect(() => {
  // Lógica del efecto
});

// Más tarde, cuando queramos destruir el efecto:
cleanup();

Al crear y usar Effects, es importante considerar su impacto en el rendimiento y asegurarse de que se utilizan de manera apropiada para manejar efectos secundarios en nuestra aplicación Angular.

¿En qué casos se utiliza un Effect?

Los Effects en Angular son una herramienta poderosa para manejar efectos secundarios de manera reactiva, pero es importante utilizarlos en los casos apropiados. Algunos escenarios comunes donde los Effects son particularmente útiles incluyen:

  1. Sincronización con APIs externas: Cuando necesitas realizar llamadas a servicios externos en respuesta a cambios en el estado de la aplicación. Por ejemplo, enviar datos actualizados a un servidor cada vez que un formulario cambia.
  2. Persistencia de datos: Para guardar automáticamente el estado de la aplicación en el almacenamiento local o en una base de datos cuando ciertos valores cambian.
  3. Manipulación del DOM: Cuando necesitas realizar cambios en el DOM que no pueden manejarse fácilmente a través del sistema de templates de Angular. Por ejemplo, modificar atributos de elementos HTML o interactuar con bibliotecas de terceros que manipulan el DOM directamente.
  4. Navegación programática: Para implementar lógica de navegación compleja basada en cambios en el estado de la aplicación, como redirigir a un usuario después de que se complete una acción específica.
  5. Logging y analytics: Para registrar eventos o enviar datos de análisis en respuesta a cambios específicos en el estado de la aplicación.
  6. Gestión de suscripciones: Para manejar suscripciones a observables de manera reactiva, iniciando o cancelando suscripciones basadas en cambios en el estado.
  7. Actualización de metadatos de la aplicación: Por ejemplo, cambiar dinámicamente el título de la página o las metaetiquetas basándose en la ruta actual o el contenido visualizado.
  8. Caching y prefetching: Para implementar estrategias de caché o precarga de datos basadas en el comportamiento del usuario o cambios en el estado de la aplicación.
  9. Integración con APIs del navegador: Cuando necesitas interactuar con APIs del navegador como Geolocation, Web Storage, o Notifications en respuesta a cambios en el estado de la aplicación.
  10. Manejo de temporizadores y animaciones: Para iniciar, detener o modificar temporizadores o animaciones basándose en cambios en el estado de la aplicación.

Cuando no usar Effects

Se debe evitar el uso de Effects para la propagación de cambios de estado. Esto puede resultar en errores de ExpressionChangedAfterItHasBeenChecked, actualizaciones circulares infinitas o ciclos innecesarios de detección de cambios. Estos problemas pueden afectar seriamente el rendimiento y la estabilidad de la aplicación.

Por ejemplo, no deberías usar Effects para actualizar un signal basado en el valor de otro signal. En su lugar, se recomienda usar computed signals para modelar estado que depende de otro estado. Esto permite a Angular manejar las actualizaciones de manera más eficiente y evita los problemas mencionados anteriormente.

Contexto de inyección para poder usar Effects

Para utilizar Effects en Angular, es crucial entender el contexto de inyección en el que operan. Los Effects están estrechamente ligados al sistema de inyección de dependencias de Angular y al ciclo de vida de los componentes y servicios.

Los Effects se pueden crear en cualquier lugar donde tengamos acceso al contexto de inyección de Angular. Esto incluye componentes, directivas y servicios. Sin embargo, el lugar donde creamos un Effect determina su ciclo de vida y su alcance.

En componentes y directivas, los Effects se crean típicamente en el constructor o en el método ngOnInit. Estos Effects estarán vinculados al ciclo de vida del componente o directiva, y se destruirán automáticamente cuando el componente o directiva sea destruido. 

Por ejemplo:

@Component({
  selector: 'app-example',
  template: '...'
})
export class ExampleComponent implements OnInit {
  constructor() {
    effect(() => {
      // Este Effect está vinculado al ciclo de vida del componente
    });
  }

  ngOnInit() {
    effect(() => {
      // Este Effect también está vinculado al ciclo de vida del componente
    });
  }
}

En servicios, los Effects pueden tener un alcance más amplio, dependiendo del alcance del servicio. Si el servicio es proporcionado a nivel de root, los Effects creados en él existirán durante toda la vida de la aplicación. Si el servicio está proporcionado a nivel de módulo o componente, los Effects seguirán el ciclo de vida de ese módulo o componente.

@Injectable({
  providedIn: 'root'
})
export class GlobalService {
  constructor() {
    effect(() => {
      // Este Effect existirá durante toda la vida de la aplicación
    });
  }
}

Es importante tener en cuenta que los Effects necesitan acceso al contexto de inyección de Angular para funcionar correctamente. Esto significa que no podemos crear Effects fuera de las clases de Angular o en funciones estáticas.

Para casos especiales donde necesitamos crear Effects fuera del contexto normal de inyección, Angular proporciona la función inject(). Esta función nos permite obtener el contexto de inyección actual y crear Effects de manera segura:

import { effect, inject } from '@angular/core';

function createCustomEffect() {
  // Obtenemos el contexto de inyección actual
  const injector = inject(Injector);

  // Creamos el Effect utilizando el contexto de inyección
  return effect(() => {
    // Lógica del Effect
  });
}

Para crear un Effect fuera del constructor, se puede pasar un Injector a effect via sus opciones:

@Component({...})
export class EffectiveCounterComponent {
  readonly count = signal(0);
  constructor(private injector: Injector) {}
  initializeLogging(): void {
    effect(() => {
      console.log(`The count is: ${this.count()}`);
    }, {injector: this.injector});
  }
}

Este enfoque es útil cuando necesitas crear Effects en métodos que se llaman después de la inicialización del componente o en situaciones donde no tienes acceso directo al contexto de inyección.

Cuando trabajamos con Effects en pruebas unitarias, es importante tener en cuenta el contexto de inyección. Podemos usar TestBed para configurar el entorno de pruebas y proporcionar los servicios necesarios:

import { TestBed } from '@angular/core/testing';
import { effect } from '@angular/core';

describe('EffectTest', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      // Configuración del módulo de pruebas
    });
  });

  it('should create an effect', () => {
    const component = TestBed.createComponent(MyComponent).componentInstance;
    expect(() => effect(() => {})).not.toThrow();
  });
});

Entender el contexto de inyección para los Effects nos permite utilizarlos de manera efectiva y segura en nuestras aplicaciones Angular, asegurándonos de que se creen y destruyan correctamente según el ciclo de vida de los componentes y servicios en los que se utilizan.

Destruir Effects

Los Effects en Angular se destruyen automáticamente cuando el componente o servicio que los contiene es destruido. Sin embargo, en algunas situaciones, puede ser necesario destruir un Effect manualmente antes de que su contexto de inyección sea destruido. Esto es particularmente útil cuando se necesita un control más granular sobre el ciclo de vida de los Effects o cuando se crean Effects dinámicamente.

Para destruir un Effect manualmente, podemos utilizar la función de limpieza que devuelve la función effect(). Esta función de limpieza, cuando se invoca, detiene la ejecución del Effect y lo desvincula de sus dependencias. 

Aquí tienes un ejemplo de cómo crear y destruir un Effect manualmente:

import { Component, effect, EffectRef, inject, Injector, runInInjectionContext, signal } from '@angular/core';

@Component({
  selector: 'app-example',
  template: '    
<button (click)="toggleEffect()">{{ effectActive() ? 'Destroy' : 'Create' }} Effect</button>
<button (click)="incrementCounter()">Increment Counter ({{ counter() }})</button>
    '
})
export class ExampleComponent {
 counter = signal(0);
  effectActive = signal(false);
  private effectRef: EffectRef | null = null;
  private injector = inject(Injector);

  toggleEffect() {
    if (this.effectActive()) {
      this.destroyEffect();
    } else {
      this.createEffect();
    }
    this.effectActive.update(active => !active);
  }

  private createEffect() {
    runInInjectionContext(this.injector, () => {
      this.effectRef = effect(() => {
        console.log(`Counter value: ${this.counter()}`);
      });
    });
    console.log('Effect created');
  }

  private destroyEffect() {
    if (this.effectRef) {
      this.effectRef.destroy();
      this.effectRef = null;
      console.log('Effect destroyed');
    }
  }

  incrementCounter() {
    this.counter.update(value => value + 1);
  }

  ngOnDestroy() {
    this.destroyEffect();
  }
}

Este ejemplo demuestra cómo:

  • Crear manualmente un efecto cuando se hace clic en el botón "Create Effect".
  • Destruir manualmente el efecto cuando se hace clic en el botón "Destroy Effect".
  • El efecto registra el valor del contador cada vez que cambia, pero solo cuando el efecto está activo.
  • El efecto se limpia adecuadamente cuando el componente se destruye.

Es importante tener en cuenta que la destrucción manual de efectos debe usarse con cuidado, ya que puede llevar a comportamientos inesperados si no se maneja correctamente. En la mayoría de los casos, es preferible dejar que Angular maneje el ciclo de vida de los efectos automáticamente.

Para Effects que necesitan ser creados y destruidos dinámicamente, puedes mantener una colección de funciones de limpieza:

private effectCleanups: (() => void)[] = [];

createDynamicEffect() {
  const cleanup = effect(() => {
    // Lógica del Effect
  });
  this.effectCleanups.push(cleanup);
}

destroyAllEffects() {
  this.effectCleanups.forEach(cleanup => cleanup());
  this.effectCleanups = [];
}

ngOnDestroy() {
  this.destroyAllEffects();
}

Este enfoque te permite crear múltiples Effects dinámicamente y asegurarte de que todos se destruyan adecuadamente cuando sea necesario.

Recuerda que la destrucción manual de Effects es una operación avanzada y, en la mayoría de los casos, permitir que Angular maneje automáticamente el ciclo de vida de los Effects es suficiente y más seguro. Solo deberías considerar la destrucción manual cuando tengas requisitos específicos que no puedan ser satisfechos por el comportamiento predeterminado de Angular.

Se puede usar la opción manualCleanup para crear Effects que duran hasta que se destruyen manualmente, pero se debe tener cuidado de limpiarlos cuando ya no sean necesarios.

Aprendizajes de esta lección

   - Comprender qué son los Effects en Angular.

   - Aprender a crear y usar effects en componentes y servicios.

   - Controlar el ciclo de vida de los effects, incluyendo su destrucción manual.

   - Utilizar el contexto de inyección para trabajar con effects.

Completa Angular y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración