In Angular, it is possible to load and view components dynamically at runtime by calling viewContainerRef.createComponent(factory)
on an instance of ViewContainerRef
, passing a factory that can create an instance of the component.
By passing an Injector
instance as third argument, it is possible to provide additional services (programmatically) to the dynamically loaded component (and its sub-components), e.g.:
const factory = factoryResolver.resolveComponentFactory(MyComponent);
const injector = Injector.create({
providers: [
{ provide: AdditionalService, useClass: AdditionalService },
],
parent: parentInjector
});
const componentRef = viewContainerRef.createComponent(factory, undefined, injector);
However, the additional service is only instantiated, if the dynamically created component needs it - so we don't know, if the injector holds an instance of this service yet.
Some time later, we destroy the dynamically created component:
// some time later, we destroy the dynamically created component:
componentRef.destroy();
The question is: What happens to the (probably existing) service when the component is destroyed (and the service has an ngOnDestroy()
method)?
Unfortunately, destroying the component does not destroy the (possibly existing) service automatically! Also the injector does not provide a method for destruction (e.g. injector.destroy()
), so it is not possible to destroy the additional service.
How can we maintain the lifecycle (especially ngOnDestroy()
) of those programmatically provided services correctly?
Note: I've implemented a short example on StackBlitz that demonstrates this behavior. It loads a component dynamically that requires two services. The first service is provided on component level (@Component({ provides: [ FirstService ]}
), the second via injector as described above. When the component is destroyed, the first service is destroyed correctly while the second "stays alive".
In my point of view, Angular's Injector API misses a mechanism to maintain the lifecycle of all services that have been instantiated within the scope of the injector. Fortunately, we can use ComponentRef.onDestroy(...)
to destroy the additional service by ourselves:
const factory = factoryResolver.resolveComponentFactory(MyComponent);
const injector = Injector.create({
providers: [
{ provide: AdditionalService, useClass: AdditionalService },
],
parent: parentInjector
});
const componentRef = viewContainerRef.createComponent(factory, undefined, injector);
// register callback function to be called on destruction of MyComponent
componentRef.onDestroy(() => {
injector.get(AdditionalService).ngOnDestroy();
});
This approach has one big disadvantage: If MyComponent
does not require an AdditionalService
, the injector won't instantiate it. However, as our onDestroy
-callback function queries the service from the injector, it will be created anyway (due to injector.get(AdditionalService)
) - just to be destroyed immediately!
So we must only get and destroy the service, if it has been created before. By using a provider factory, we can intercept service creation and do the required bookkeeping:
const factory = factoryResolver.resolveComponentFactory(MyComponent);
const destructables = new Set<OnDestroy>();
const injector = Injector.create({
providers: [
{
provide: AdditionalService,
useFactory: () => {
const service = new AdditionalService();
destructables.add(service);
return service;
}
},
],
parent: parentInjector
});
const componentRef = viewContainerRef.createComponent(factory, undefined, injector);
// register callback function to be called on destruction of MyComponent
componentRef.onDestroy(() => {
try {
destructables.forEach(obj => obj.ngOnDestroy());
} finally {
destructables.clear();
}
});
With this approach, we can programmatically provide service instances on a per-component level and still maintain the lifecycle and call ngOnDestroy()
when the service is not needed any more.
Top comments (0)