Leveraging injection tokens to solve circular dependencies in shared libraries
By: Ido Mor
Tags: Frontend Development, Injectiontoken, Nx, Angular

In the past year, my team and I have been busy building a shareable Angular library for the use of other developers in the organization.

In the process of doing so, we’ve faced lots of challenges of how to develop generic reusable services and components. We’ve also had to implement some interesting use cases of Angular’s injection tokens, in order to solve a variety of problems, like

circular dependency between our libraries

loose coupling between the hosting application and consumed library

If you’ve ever built, or are looking to build, an Angular library and you find yourself refactoring over and over again just to solve dependency issues, You came to the right place!.

Moreover, if you feel that you are well familiar with Angular’s injection tokens, but have the feeling that you can do more with them, then this post is definitely for you.

Ecosystem

As part of my work in Yotpo’s platform group, we aim to build capabilities for all of Yotpo’s current and future products. Those capabilities should reduce both the developers’ and customers’ learning curves while interacting with Yotpo’s components.

From the developers’ point of view, we aim to create Plug & Play building blocks, so that our developers will be able to increase their velocity.

For that purpose, we’ve built an Angular mono repo, managed by NX, we use semantic versioning, and publish our artifacts as npm packages to Jfrog.

To make our artifacts as generic as possible, we depend heavily on injection tokens to help the hosting app provide each lib with everything it has to know to perform its goal.

Plenty had been written about Angular Injection tokens, and if this technique is not familiar to you, then you should definitely read about it here. But the short version of Injection tokens (to me) is that this is a mechanism that gives you the ability to inject something other than an angular service, even something which is not a class.

Basic usage of injection tokens

Let’s examine how MeaningfulService exists in the core lib.

For this service to be generic, it is dependent on an injected configuration, so…

export interface MeaningfulServiceConfig {

meaningfulProp: string;

}

we expose an injection token for the MeaningfulModel…

import {InjectionToken} from '@angular/core';

import {MeaningfulServiceConfig} from './models/meaningful-service-config';



export const MEANINGFUL_CONFIG_INJECTION_TOKEN = new InjectionToken<MeaningfulServiceConfig>('MeaningfulServiceConfig');

and we consume this token in the constructor of the service:

import {Inject, Injectable} from '@angular/core';

import {MEANINGFUL_CONFIG_INJECTION_TOKEN} from '../meaningful-config-injection-token';

import {MeaningfulServiceConfig} from '../models/meaningful-service-config';



@Injectable({

providedIn: 'root'

})

export class MeaningfulService {



constructor(@Inject(MEANINGFUL_CONFIG_INJECTION_TOKEN) private config: MeaningfulServiceConfig) { }

}

This is all good for the core lib.

As you might have noticed, we have 2 more libraries that depend on the core library by using MeaningfulService.

import {Inject, Injectable} from '@angular/core';

import {MeaningfulService} from '../../../../core/src/lib/services/meaningful.service';

import {LIB_A_CONFIG_INJECTION_TOKEN} from '../lib-a-injection-token';

import {LibAConfig} from '../models/lib-a-config';



@Injectable()

export class ServiceAService {



constructor(private meaningfulService: MeaningfulService,

@Inject(LIB_A_CONFIG_INJECTION_TOKEN) private libAConfig: LibAConfig) { }



doSomething() {};

}

There are a few things to note here:

The service depends on both the core’s MeaningfulService and Lib-A injected configuration

The service is not provided in root, since it is part of a library that is designed to be lazy-loaded by the hosting application. Thus the service needs to be provided by an Angular module

export interface LibAConfig {

libAProp: string;

} import {InjectionToken} from '@angular/core';

import {LibAConfig} from './models/lib-a-config';





export const LIB_A_CONFIG_INJECTION_TOKEN = new InjectionToken<LibAConfig>('LibAConfig'); @NgModule({

imports: [CommonModule],

providers: [ServiceAService,]

})

export class LibAModule {}

Now that our library is wrapped up, we can consume its Angular module in an Angular application. The module’s configuration also needs to be provided.

@NgModule({

declarations: [AppComponent],

imports: [BrowserModule, LibAModule],

providers: [

{

provide: LIB_A_CONFIG_INJECTION_TOKEN,

useValue: LibAConfigProvider

}

],

bootstrap: [AppComponent],

})

export class AppModule {} import {LibAConfig} from '../../../../libs/lib-a/src/lib/models/lib-a-config';



export const LibAConfigProvider: LibAConfig = {

libAProp: 'hello lib a'

} export class AppComponent {

constructor(private service: ServiceAService) {

service.doSomething();

}

}

But oh no! We have a runtime error.

NullInjectorError — occurs due to a missing provider: ‘MeaningfulServiceConfig’

Ok, this one is easy to solve right?

We can provide the MeaningfulService configuration the same way we’d provided the Lib-A configuration, right? Here’s what I don’t like and wish to solve in this approach:

The hosting application that uses the Lib-A module needs to be familiar with Lib-A dependencies — MeaningfulService and its configuration. My main concern is that if tomorrow I replace the use of MeaningfulService in Lib-A with MeaningfulService2 (together with its own injection token), then this will be considered a breaking change and the hosting application will have to change its AppModule.

Moreover, as Lib-A expands and becomes more depended upon and requires more injection tokens, the hosting application would have to be extremely familiar with each dependency to provide its configuration.

With that in mind, let’s define the requirements for our solution.