Mastering the Life of an Effect: Injection Context and Beyond
To understand why an effect() behaves the way it does, we have to look at the "hidden" environment it lives in. In Angular, an effect isn't just a function; it’s a managed resource that depends on the framework's Dependency Injection (DI) system to survive and clean up after itself.
1. 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.
2. Injection Context: The "Where" and "When"
Injection Context is a specific state during code execution where the inject() function is available. Also think of it as the period of time when the object(Component or Directive etc) is 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 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.
3. 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 (like 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
}
To fix this, you have to "capture" the injector manually:
export class MyComponent {
private injector = inject(Injector); // Capture the injector in the constructor
startManualEffect() {
// We pass the captured injector to the effect
effect(() => {
console.log('Manual effect running!');
}, { injector: this.injector });
}
}
4. 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
If you are building a custom utility that uses effects, you might use DestroyRef like this:
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 you don't want to pass it as an option to every single function. You can "force" a context:
import { runInInjectionContext } from '@angular/core';
runInInjectionContext(this.injector, () => {
// Anything called in 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!!!
Top comments (0)