loading...
Cover image for Angular - lazy load single component

Angular - lazy load single component

hopemanryan profile image Ryan Hoffman ・5 min read

Building an Angular app with scale in mind is tricky. We are already accustomed to lazy loading routes and by that decreasing bundle size and decreasing initial load times and letting the user interact with our web/app quicker.
With in time our Web-App will have to do more and more, which will effect the page load time and this can become extremely noticeable when building very large and dynamic forms with dynamic changing parts.
If we could just load the components that are needed in the current form and not all of them at once, load time will decrease and also we haven't exposed unnecessary code to the client (Its still there in the js files, just the UI doesn't render it).

So now that we have gone through with the examples and some benefits how is this done? Angular is primary a very closed framework, no easy workarounds which ensures the validity of the framework and to ensure build quality at all times.
But there is a still a way, an Angular way even.

@Component({
    selector: 'app-parentMock',
    template: ``,

  })
export  class ParentComponent implements OnInit {
    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
    ) {}
}

CompoentFactoryResolver is the a class Angular exports in order to create components in run time. It has some quirky behaviors but lets continue with the example.


@Component({
    selector: 'app-parentMock',
    template: ``,

  })
export  class ParentComponent implements OnInit {

    demoObj = {
        demo: {
            load: () => import('../mock/mock.component')
        }
    }

    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
    ) {}

    async ngOnInit(): Promise<void> {
        await this.loadComponent();
      }


      async loadComponent() {
          /** This saves loads the raw un-angular data into the loadFile */ 
          const loadFile: {default: any}  = await this.demoObj.demo.load(); 

      }

}

We have the object with the relative path of the component we want to load and as you can see in the snippet above there is the type {default: any} of the variable loadFile. This will not have a value at first. In order to do so in the component you are lazy loading at the bottom of the component (outside of it though) add :

This is extremely important

export default MockComponent 

Now comes the tricky part which I will explain more on


@Component({
    selector: 'app-parentMock',
    template: `
        <ng-template #lazyTab></ng-template>
    `,

  })
export  class ParentComponent implements OnInit {
    /** The html element we will be loading the component into */
    @ViewChild('lazyTab', {static: true}) lazyTab: ViewContainerRef;

    lazyLoadedCompoent: ComponentRef<any>;


    demoObj = {
        demo: {
            load: () => import('../mock/mock.component')
        }
    }

    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
        private injector: Injector,

    ) {}

    async ngOnInit(): Promise<void> {
        await this.loadComponent();
      }


      async loadComponent() {
          /** This saves loads the raw un-angular data into the loadFile */ 
          const loadFile: {default: any}  = await this.demoObj.demo.load(); 

          /** This loads the Angular component into the the varibale for later use */
          const actualComponent = this.componentFactoryResolver.resolveComponentFactory(loadFile.default);


          const viewRef: ViewContainerRef = this.lazyTab.viewContainerRef;

          /** Clear any existing html inside of of the ng-container */
          viewRef.clear()

          /** We both insert the component in to the ref and save it for later use
           * 
           *  Adding the injector is to let it load other requiered things like services and other dependecies it might have
           */
          this.lazyLoadedCompoent = viewRef.createComponent<any>(actualComponent, null, this.injector)

      }

}

Lets go over that last snippet

lazyTab(In the html) : This is template reference variable which we will use in order to tell angular where to insert that lazyLoaded component

@ViewChild('lazyTab' .... : Here we giving access to typescript to work with the template reference variable

loadFile: a variable created to save the created RAW component

actualComponent : The Angular component that we have created in runtime

Now that we have our component loaded we might want to add INPUTs or OUTPUTs to the component to keep it in sync with our entire app.
Before continuing I feel the need to talk about Angular change detection and how NgZone is the main black magic in all of Angular's Magic.
NgZone is what makes the app react to changes and update it self. It works in a way matter of scopes. and if you are working outside of the Angular scope, your changes won't be detected and therefor no UI update.



@Component({
    selector: 'app-parentMock',
    template: `
        <ng-template #lazyTab></ng-template>
    `,

  })
export  class ParentComponent implements OnInit {
    /** The html element we will be loading the component into */
    @ViewChild('lazyTab', {static: true}) lazyTab: ViewContainerRef;

    lazyLoadedCompoent: ComponentRef<any>;


    demoObj = {
        demo: {
            load: () => import('../mock/mock.component')
        }
    }

    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
        private injector: Injector,
        private zone: NgZone,
    ) {}

    async ngOnInit(): Promise<void> {
        await this.loadComponent();
      }


      async loadComponent() {
          /** This saves loads the raw un-angular data into the loadFile */ 
          const loadFile: {default: any}  = await this.demoObj.demo.load(); 

          /** This loads the Angular component into the the varibale for later use */
          const actualComponent = this.componentFactoryResolver.resolveComponentFactory(loadFile.default);

          const viewRef: ViewContainerRef = this.lazyTab.viewContainerRef;

          /** Clear any existing html inside of of the ng-container */
          viewRef.clear()

          /** We both insert the component in to the ref and save it for later use
           * 
           *  Adding the injector is to let it load other requiered things like services and other dependecies it might have
           */
          this.lazyLoadedCompoent = viewRef.createComponent<any>(actualComponent, null, this.injector)

          /** To ensure the next changes are kept inside the Angular Zone Scope */
          this.zone.run(() => {
              this.lazyLoadedCompoent.instance['any-INPUT-you want'] = 'Lazy Loaded Component'
          })
      }
}

the zone.run... will make it so that the changes in side the lazyLoaded component will be detectedrun ngOnChanges when you set/reset those INPUTs.

So now how about OUTPUTS ? well OUTPUTS are functions we pass on so how will that be done ?


@Component({
    selector: 'app-parentMock',
    template: `
        <ng-template #lazyTab></ng-template>
    `,

  })
export  class ParentComponent implements OnInit {
    /** The html element we will be loading the component into */
    @ViewChild('lazyTab', {static: true}) lazyTab: ViewContainerRef;

    lazyLoadedCompoent: ComponentRef<any>;


    demoObj = {
        demo: {
            load: () => import('../mock/mock.component')
        }
    }

    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
        private injector: Injector,
        private zone: NgZone,
    ) {}

    async ngOnInit(): Promise<void> {
        await this.loadComponent();
      }


      async loadComponent() {
          /** This saves loads the raw un-angular data into the loadFile */ 
          const loadFile: {default: any}  = await this.demoObj.demo.load(); 

          /** This loads the Angular component into the the varibale for later use */
          const actualComponent = this.componentFactoryResolver.resolveComponentFactory(loadFile.default);


          const viewRef: ViewContainerRef = this.lazyTab.viewContainerRef;

          /** Clear any existing html inside of of the ng-container */
          viewRef.clear()

          /** We both insert the component in to the ref and save it for later use
           * 
           *  Adding the injector is to let it load other requiered things like services and other dependecies it might have
           */
          this.lazyLoadedCompoent = viewRef.createComponent<any>(actualComponent, null, this.injector)

          /** To ensure the next changes are kept inside the Angular Zone Scope */
          this.zone.run(() => {
            /** INPUT */  
            this.lazyLoadedCompoent.instance['any-INPUT-you want'] = 'Lazy Loaded Component'

            /**  OUTPUT */
            this.lazyLoadedCompoent.instance['an-OUTPUT-type-of-new-Emitter'].subscribe((dataPassedByTheEmit: any) => {
                console.log(dataPassedByTheEmit);
                /** Do what ever you want wit it */
            })
        })
      }
}

So the OUTPUT is of type Emitter which means we can subscribe to it and get the data which is emitted from the lazyLoaded component.

This is amazing , we have a fully living component that was loaded in runtime by the app.

Lets talk about the downsides first

  1. This requires a lot of overhead and ability write maintainable code.
  2. Doing this for a number of components that can change will require more overhead and a way to keep things updated when the user changes between components
  3. This is not a very Angular way of doing things
  4. Components are still loaded in the module.
  5. Bundle size is not decreased

upsides:

  1. Decrease load time on extremely large forms or pages.
  2. Ability to load a component in reference to the type of user that is signed in
  3. When you want to have the option to load components from a server
  4. running A/B testing
  5. Super cool idea that has been tested in production for a very large scale application.

Hope you enjoyed this small tutorial.
For any questions or comments feel free to comment and I will be happy to reply

Discussion

pic
Editor guide