DEV Community

mwinterer
mwinterer

Posted on

Provide services to dynamic angular components

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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();
    }
});
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)