Angular

Lazy Load Services in Angular

Imagine we have a large service with many dependencies and logic. It should only be used when a user performs a specific...

Written by Luci · 1 min read >

Imagine we have a large service with many dependencies and logic. It should only be used when a user performs a specific action, such as clicking a button.

@Injectable({
  providedIn: 'root'
})
export class BigService {
  // ....
  
  doSomething() {
    // ....
  }
}
@Component({
  template: `
    <button (click)="doSomething()">Action</button>
  `
})
export class FooComponent {
  constructor(private service: BigService) {}
  
  doSomething() {
    this.service.doSomething();
  }
}

It’s possible to defer the loading and parsing of the service by lazy loading it when the action is performed.

Let’s create a lazy inject service:

@Injectable({ providedIn: 'root' })
class LazyInject {
  constructor(private injector: Injector) {}

  async get<T>(providerLoader: () => Promise<ProviderToken<T>>) {
    return this.injector.get(await providerLoader());
  }
}

The get() method expects a provider token loader — a promise that returns a provider token. A reference to the provider is retrieved from the injector when the provider is loaded. Let’s use our service:

import type { BigService } from './big-service.service';

@Component({
  template: `
    <button (click)="doSomething()">Action</button>
  `
})
export class FooComponent {
  constructor(private lazyInjector: LazyInject) {}
  
  async doSomething() {
    const service = await this.lazyInjector.get<BigService>(() =>
      import('./big-service.service').then((m) => m.BigService)
    );
    
    service.doSomething()
  }
}

And that’s all. We can take it one step further. Imagine we have several strategies and need to choose one based on configuration. Each strategy contains a lot of code. Adding every strategy to the main bundle would be wasteful since all clients would have to download and parse it, even if they didn’t need it.

We can lazy load the required service in runtime and make it available to our component through the injector. Let’s see a simple example of using a storage provider:

export abstract class Storage {
  abstract get(): string;
  // ....
}
import { Storage } from './storage';

export default class LocalStorage extends Storage {
  get() {
    return 'local';
  }
}

import { Storage } from './storage';

export default class SessionStorage extends Storage {
  get() {
    return 'session';
  }
}

All files under storage directory

Now we can create a directive that’ll lazy load the required service based on a configuration, create an injector, and pass it to the component:

@Directive({
  selector: '[container]'
})
export class ContainerDirective {
  constructor(
     private userConfig: UserConfig,
     private vcr: ViewContainerRef, 
     private injector: Injector
  ) { }

  async ngOnInit() {
    const StorageProvider = await import(`./storage/${this.userConfig.storage}-storage`)
                                        .then(m => m.default)

    const injector = Injector.create({
      providers: [{ provide: Storage, useClass: StorageProvider }],
      parent: this.injector,
    });

    this.vcr.createComponent(MyComponent, { injector });
  }
}

MyComponent doesn’t care about the implementation details. It injects the Storage token and uses it.

Written by Luci
I am a multidisciplinary designer and developer with a main focus on Digital Design and Branding, located in Cluj Napoca, Romania. Profile

Angular Basics: The CLI and Components

Luci in Angular
  ·   7 min read
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x