Provide services to dynamic angular components
Tags: angular
Published: 2021-08-24 22:26:48

In Angular, it is possible to [load and view components dynamically at runtime][1] 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.:

```typescript
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:

```typescript
// 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][2] 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:

```typescript
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:

```typescript
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.

  [1]: https://angular.io/guide/dynamic-component-loader
  [2]: https://stackblitz.com/edit/angular-ivy-v4gwqa