Effects and InjectionContext in Angular(v21)
Source: Dev.to
The Injector: The “Source of Truth”
The Injector is a container that holds instances of services and dependencies. In Angular, there is a hierarchy of injectors (Environment Injector, Component Injector, etc.).
When you create an effect, it needs to be anchored to an Injector. The Injector provides the effect with the services it might need and, more importantly, determines the lifetime of the effect.
Injection Context: The “Where” and “When”
Injection Context is a specific state during code execution where the inject() function is available. Think of it as the period of time when the object (Component, Directive, etc.) is being constructed.
By default, you have an Injection Context in:
- The constructor of a class.
- Field initializers (e.g.,
myService = inject(MyService)). - The factory function of a Provider.
Why does an effect need it?
When you call effect(() => { … }) in a constructor, Angular implicitly looks for the current Injection Context to find and “inject” the DestroyRef. It uses this to know exactly when to “kill” the effect so it doesn’t cause memory leaks.
The inject() Function: The Modern Fetcher
The inject() function is the programmatic way to get a dependency from the current Injection Context.
If you try to create an effect inside a regular method (e.g., a button click), it will fail because that method doesn’t have an Injection Context.
// ✅ WORKS: In the constructor / class init
count = signal(0);
myEffect = effect(() => console.log(this.count()));
// ❌ FAILS: Outside injection context
updateData() {
effect(() => { /* … */ }); // Error: inject() must be called from an injection context
}
Capturing the injector manually
export class MyComponent {
private injector = inject(Injector); // Capture the injector in the constructor
startManualEffect() {
// Pass the captured injector to the effect
effect(() => {
console.log('Manual effect running!');
}, { injector: this.injector });
}
}
DestroyRef: The “Clean‑up Crew”
DestroyRef is a modern replacement for the OnDestroy lifecycle hook. It allows you to register cleanup logic anywhere in your code, not just in the class definition.
When an effect is created, it internally subscribes to the DestroyRef of its context. When the component or service holding that effect is destroyed, the DestroyRef fires, and the effect is automatically stopped.
Manual cleanup with DestroyRef
const dr = inject(DestroyRef);
const sub = mySignal.subscribe(...);
dr.onDestroy(() => {
console.log('Cleaning up resources!');
sub.unsubscribe();
});
The runInInjectionContext Helper
Sometimes you have an Injector but don’t want to pass it as an option to every function. You can “force” a context:
import { runInInjectionContext } from '@angular/core';
runInInjectionContext(this.injector, () => {
// Anything called here now has access to inject()
effect(() => console.log('I have context!'));
});
Code Playground
Key Points to Remember
- Effects are tied to the Injector.
- Injectors use DestroyRef to clean up.
inject()only works when the Injection Context is active.- If you create an effect outside a constructor, you must provide the injector manually.
Happy coding!