Avoiding Duplication Injectable Instances in Angular

This topic has been repeatedly described in other articles and documentation, but when I faced this problem, I still had to collect information bit by bit and do a lot of test examples on Stackblitz to get to know how to do it right, so I decided to write this article as a note for myself in future.

Intro

If you want to create only one instance of any service for a whole application, you want to create a Singleton

Why you may want it?

The most common reason is to share some valuable state between all parts of your application.

Take a look at simple application configuration service:

@Injectable()
export class SettingsService {
  private settings = new Map();

  public get(key: string): any {
    return this.settings.get(key);
  }

  public set(key: string, value: any): any {
    return this.settings.set(key, value);
  }
}

Configuration service called SettingsService

and it’s module:

@NgModule({
  imports: [BrowserModule],
  declarations: [ApplicationComponent],
  bootstrap: [ApplicationComponent],
  providers: [SettingsService]
})
export class AppModule {}

AppModule with SettingsService

Basically I want to use the same settings for a whole application:

@Component({
  selector: 'app',
  template: ''
})
class ApplicationComponent {
  constructor(private settings: SettingsService) {
    settings.set('FEATURE', true);
  }
}

Application component sets configuration

And then use it in some component:

this.isFeatureAvailable = settings.get('FEATURE');
...
<div *ngIf="isFeatureAvailable"><super-feature></super-feature><div>

Typical usage of app configuration

But sometimes Angular can create more than one instance of SettingsService, so your settings will vary by instance and it will lead to serious configuration problems in your application.

Well, let’s see why this is happening and how to handle it.

The problem

Angular will create new instances for any of InjectionToken or Injectable in cases of using:

This is happening because Angular creates a new module  Injector for any lazy loaded module.

Here is the demo with the problem demonstration.

The solutions

The most important thing to understand here — adding any Injectable (or InjectionToken) to the @NgModule.providers list for any Eager and Lazy module pair will duplicate such Injectable!

So, first step is not to add services that should be singletons to @NgModule.providers list of any module.

Basically you can add service to the Application module providers and it will work. But other developers may not know that you want to use this service as a singleton, and somebody will add this service to the providers list of lazy loaded module and Angular will create second instance of it.

You should choose which strategy to use, because there are two with its’ pros and cons:

  • static **forRoot**() @NgModule method
  • @Injectable({ **providedIn**: ‘root’ })

forRoot()

forRoot method is a kind of agreement/convention between Angular developers to call this method in the root module only (AppModule for example), so any service will be provided only once.

For this technique you should create module and implement static forRoot(): ModuleWithProviders method.

Example:

@NgModule({
  imports: [CommonModule]
})
export class SettingsModule {
  public static forRoot(): ModuleWithProviders {
    return {
      ngModule: SettingsModule,
      providers: [SettingsService]
    };
  }
}

SettingsModule with forRoot

Note: forRoot method is not handled by any code in Angular Compiler so you can name it as you want (forMySuperHotRootAppModule()?), but it is not recommended.

Here is the demo with forRoot solution.

providedIn: ‘root’

When you mark Injectable as provided in root, Angular resolver will know that such Injectable, used in lazy module, was added to the root module, and will look for it in the root injector, not newly created lazy loaded module injector (default behavior).

@Injectable({
  providedIn: 'root'
})
export class SettingsService {
  private settings = new Map();

  public get(key: string): any {
    return this.settings.get(key);
  }

  public set(key: string, value: any): any {
    return this.settings.set(key, value);
  }
}

SettingsService with providedIn: ‘root’

Huge plus of this solution — angular’s ability to use tree shaking with providedIn.

Also with providedIn your tests will not fail, because if all of your services provided in root (99,99% should be provided in root I bet) TestBed will resolve it correctly.

Here is the demo with providedIn solution.

The singleton guard

You can easily get to know if somebody created the second instance of your service.

@Injectable({
  providedIn: 'root'
})
export class GuardedSingletonService {
  constructor(@Optional() @SkipSelf() parent?: GuardedSingletonService) {
    if (parent) {
      throw Error(
        `[GuardedSingletonService]: trying to create multiple instances,
        but this service should be a singleton.`
      );
    }
  }
}

Throws error if instance already exists

Also it can be done as a base class:

export class RootInjectorGuard {
  constructor(type: Type<any>) {
    const parent = inject(type, InjectFlags.Optional | InjectFlags.SkipSelf);

    if (parent) {
      throw Error(`[${type}]: trying to create multiple instances,
      but this service should be a singleton.`);
    }
  }
}

Guard for singleton services

Usage:

@Injectable({
  providedIn: 'root'
})
export class MySingletonService extends RootInjectorGuard {
  constructor() {
    super(MySingletonService);
  }
}

Now more than one instance of MySingletonService can’t be created

Bonus

Here are some questions I searched answers for when faced this issue.

How to handle it with InjectionToken?
InjectionToken have options as second argument .

class MyDep {}

class MyService {
  constructor(readonly myDep: MyDep) {}
}

const MyServiceToken = new InjectionToken('MyToken', {
  providedIn: 'root',
  factory: () => new MyService(inject(MyDep))
});

InjectionToken with providedIn: ‘root’

What if I use forRoot in combination with providedIn: 'root'?
There is no differences between using: forRoot or providedIn or forRoot+providedIn. Service will be created only once.

What if I use forRoot and providers list?
Service will be duplicated.

What if I use providedIn and providers list?
Service will be duplicated.

Conclusion

Creating multiple instances of the same services can become a problem and Angular provides some abilities to handle it.

Thanks for reading!

#angular #angularjs

Avoiding Duplication Injectable Instances in Angular
8.35 GEEK