Entendiendo InjectionContext y los effects de Signals

Ayer estuve conversando con mi amigo @Jörgen de Groot sobre Signals. Cuando nuestra conversación pasó a hablar de los effects, se topó con el error ERROR NG0203.
Poco después, otro amigo me escribió con el mismo problema. Decidí armar un artículo corto para explicar por qué sucede.
Escenario
Tenemos una aplicación de contador que usa signals para registrar la cantidad de clics. Cuando la variable de signal count llega a 10, debemos actualizar una variable local normal (sin signals) con el texto 'You reached the limit of clicks'.
Usando Effect
El effect de Signals es muy útil para manejar efectos secundarios. En nuestro caso, tenemos un efecto secundario donde la variable 'limitMessage' necesita actualizarse cuando la variable 'count' llega a 10.
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<h1>{{limitMessage}}</h1>
<h2>{{ count()}} times</h2>
<button (click)="increase()">+1</button>
`,
})
export class App {
count = signal(0);
limitMessage = 'Feel free to click!';
increase() {
this.count.update((p) => p + 1);
}
}
bootstrapApplication(App);Ejecutamos el effect dentro del ciclo de vida ngOnInit para monitorear los cambios en la variable count.
export class App implements OnInit {
count = signal(0);
limitMessage = 'Feel free to click!';
ngOnInit(): void {
effect(() => {
if (this.count() == 10) {
this.limitMessage = 'You reached the limit, sorry';
}
});
}¡Rayos! ¡Apareció el error!

Effects e Injection Context
El effect debe ejecutarse dentro de un contexto de inyección (injection context), ya que utiliza el servicio DestroyRef para cancelar la suscripción cuando el bloque o componente se destruye.
El Constructor
La primera opción es mover el effect al constructor. Guardamos los cambios y ¡sí! ¡El effect funciona!

Usando runInjectionContext
Para usar el effect fuera del constructor, tenemos que utilizar la función runInInjectionContext, inyectando el Injector.
export class App implements OnInit {
count = signal(0);
limitMessage = 'Feel free to click!';
disabled = signal(false);
#injector = inject(Injector);
ngOnInit(): void {
runInInjectionContext(this.#injector, () => {
effect(() => {
if (this.count() == 10) {
this.limitMessage = 'You reached the limit, sorry';
}
});
});
}
increase() {
this.count.update((p) => p + 1);
}
}
¡Listo! ¡Tenemos el effect funcionando fuera del constructor de la clase y sin errores!
Actualizar Signals dentro de Effects
Ya estamos usando effects sin problemas, pero agreguemos una pequeña funcionalidad conectada a nuestro effect: quiero deshabilitar el botón cuando el contador llegue a 10.
Para esto, voy a crear la variable 'disabled' como una signal booleana y usar el effect para actualizar su valor.
export class App implements OnInit {
count = signal(0);
limitMessage = 'Feel free to click!';
disabled = signal(false);
#injector = inject(Injector);
ngOnInit(): void {
runInInjectionContext(this.#injector, () => {
effect(() => {
if (this.count() == 10) {
this.limitMessage = 'You reached the limit, sorry';
this.disabled.update(() => true);
}
});
});
}
increase() {
this.count.update((p) => p + 1);
}
}Guardamos los cambios e intentamos actualizar la signal disabled. ¡Maldición, otro error!

Por defecto, los effects no tienen permitido actualizar signals. Sin embargo, para estos casos podemos desactivar esta restricción configurando la opción allowSignalWrite en true.

¡Perfecto! Ya estamos trabajando con el effect de Signals sin ningún problema.
Resumen
Aprendimos cómo implementar el effect en el ciclo de vida ngOnInit, la importancia del contexto de inyección, cómo usar runInInjectionContext para evitar errores y cómo actualizar signals dentro de un effect desactivando la restricción por defecto con la opción allowSignalWrite.
¡Espero que esto te sirva cuando empieces a usar Signals y effects! ;)
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Artículos Relacionados
Primeros pasos con Angular Signals: Una introducción básica
Desde Angular 16 he escuchado mucho sobre Signals. El equipo de Angular hizo un gran trabajo escuchando a la comunidad y mejorando la API de Signals con la interoperabilidad con RxJS. Ayer estuve hablando con mi amigo Juan Berzosa y aún no había probado Signals, así que...
Angular 19 y ZoneLess
No importa si usas Angular 14, 15 o 19: tus aplicaciones siguen bajo el paraguas de Zone.js. Aunque sientas que funcionan bien, por debajo Angular necesita recorrer todo el árbol del DOM con Zone.js, lo que afecta el rendimiento.
Cómo simplificar la comunicación entre componentes en Angular con model() y Signals
Estuve muy ocupado en septiembre, pero ahora estoy preparando un taller sobre signals y explorando ideas sobre cómo pueden simplificar nuestro código. Recientemente usaba @Input() y @Output para la comunicación entre componentes, pero Angular 17 introdujo alternativas más limpias...
Deja de duplicar la lógica de validación en Next.js con Zod
Aprende a compartir esquemas de Zod entre cliente y servidor en Next.js para asegurar la integridad de los datos sin repetir código. Foto por Clément Hélardot en Unsplash
Cómo funciona el método sort en arrays de JavaScript
Estaba ayudando a un amigo a ordenar una tabla de datos en TypeScript y nos encontramos con un comportamiento inesperado. En este artículo revisamos qué ocurre en JavaScript al usar el método sort con strings y números.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.