Angular: understanding how DI works in dynamicaly loaded component

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?

9 thoughts on “Angular: understanding how DI works in dynamicaly loaded component”

Leave a Comment