Angular: understanding how DI works in dynamicaly loaded component

Viewed 126

I'm experiencing strange behavior of services injected to a component that is loaded dynamically. Consider the following service

@Injectable({
    providedIn: 'root'
})
export class SomeService {
    private random = Math.random() * 100;

    constructor() {
        console.log('random', this.random);
    }
}

The service is added as a dependency to two components. First component is a part of lazy-loaded module. While second one is loaded dynamically. The following service loads modules with dynamic component

export const COMPONENT_LIST = new InjectionToken<any>('COMPONENT_LIST');
export const COMPONENT_TYPE = new InjectionToken<any>('COMPONENT_TYPE');

@Injectable({
    providedIn: 'root'
})
export class LoaderService {
    constructor(
        private injector: Injector,
        private compiler: Compiler,
    ) { }

    getFactory<T>(componentId: string): Observable<ComponentFactory<T>> {
        // COMPONENT_LIST is passed through forRoot() from the module that declares first component
        const componentList = this.injector.get(COMPONENT_LIST); 
        const m = componentList.find(m => m.componentId === componentId);

        const promise: Promise<ComponentFactory<T>> = (!m) ? null :
            m.loadChildren
                .then((mod: any) => {
                    return this.compiler.compileModuleAsync(mod);
                })
                .then((mf: NgModuleFactory<any>) =>  {
                    const mr: NgModuleRef<any> = mf.create(this.injector);
                    const type: Type<T> = mr.injector.get<Type<T>>(COMPONENT_TYPE); // DYNAMIC_COMPONENT is provided in loaded module
                    return mr.componentFactoryResolver.resolveComponentFactory<T>(dynamicComponentType);
                });

        return from(promise);
    }
}

In the same module I declare the following component (to place component that is loaded dynamically) and directive

@Component({
    selector: 'dynamic-wrapper',
    template: `<ng-container dynamicItem></ng-container>`
})
export class DynamicWrapperComponent implements AfterViewInit, OnDestroy {
    @Input() itemId: string;
    @Input() inputParameters: any;

    @ViewChild(DynamicItemDirective)
    private dynamicItem: DynamicItemDirective;

    private unsubscribe$: Subject<void> = new Subject();

    constructor(
        private loaderService: LoaderService
    ) { }

    ngAfterViewInit(): void {
        this.loaderService.getComponentFactory(this.itemId).subscribe((cf: ComponentFactory<any>) => {
            this.dynamicItem.addComponent(cf, this.inputParameters);
        });
    }
}
...
@Directive({
    selector: '[dynamicItem]'
})
export class DynamicItemDirective {

    constructor(protected viewContainerRef: ViewContainerRef) { }

    public addComponent(cf: ComponentFactory<any>, inputs: any): void {
        this.viewContainerRef.clear();
        const componentRef: ComponentRef<any> = this.viewContainerRef.createComponent(cf);
        Object.assign(componentRef.instance, inputs);
        // if I do not call detectChanges, ngOnInit in loaded component will not fire up
        componentRef.changeDetectorRef.detectChanges();
    }
}

SomeService is defined in a separate module that is imported in both lazy-loaded module with first component and dynamically loaded module.

After both components are initialed I see output of console.log('random', this.random) with two different numbers in console, despite providedIn: 'root' in decorator. What is the reason for such a strange behavior?

1 Answers

Lazy loaded modules won't react to providedIn: 'root' as one might expect. This option will push services to the AppModule(root module) only if the module that they are imported in is not lazy loaded.

Why do you see different random numbers?

Because the module that SomeService is defined in is initiated twice! (feel free to put console.log in the constructor of this module) - therefore the service is initiated twice (I encountered this issue myself in my project lazy load platform).

Why the lazy module that contain SomeService is initiated twice?

loadChildren or moduleFactory.create(this.injector) don't hold a cache of lazy loaded modules. They initiate a module and attach its injector as a leaf to the input injector.

If the module that contains SomeService was created from moduleFactory.create - you can add a cache layer to return the cached initiated lazy loaded module.

For example:

private lazyLoadedModulesCache: Map<Type<any>, NgModuleRef<any>>;

Related