angularangular-ivy

How to destroy services that are programmatically injected into dynamically created 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 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();

Unfortunately, destroying the component does not destroy the (possibly existing) service automatically! Also the injector does not provide a method for destruction, 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".


Solution

  • I've found a solution to the problem using a minimum of boilerplate code.

    Solution: The injector created by Injector.create(...) is an R3Injector, which indeed tracks the instances and providers associated with it and also calls their lifecycle methods (ngOnDestroy()) when the injector itself is destroyed via its destroy() method. So by explicitely destroying the injector, all instances and providers get destroyed too. But as the destroy() method is not public API, we have to check for its existance first:

    // CODE FROM THE QUESTION
    const injector = Injector.create({
        providers: [
            { provide: AdditionalService, useClass: AdditionalService },
        ],
        parent: parentInjector
    });
    
    const componentRef = viewContainerRef.createComponent(factory, undefined, injector);
    
    // ADDITIONAL CODE
    // register a listener on the component reference to get notified
    // when the component gets destroyed
    componentRef.onDestroy(() => {
        // destroy injector if it is destroyable (i.e. has a destroy method)
        if (isDestroyable(injector)) injector.destroy();
    });
    

    The function isDestroyable can be implemented as type guard:

    interface Destroyable {
        destroy();
    }
    
    function isDestroyable(value: unknown): value is Destroyable {
        return value !== null && typeof value === 'object' &&
               typeof (value as Destroyable).destroy === 'function';
    }
    

    I've updated the StackBlitz project to demonstrate this solution.