Hi guys, welcome back to my blog 👋. Today, I want to share with you all about a trick that I’ve been using to access
providedIn: 'root'
Injectables
in places where Dependency Injection (DI) does not make sense in an Angular
application.
What do I mean by non-DI places? In an Angular application, there are many different building blocks like: Component, Directives, Services, and Modules etc. These building blocks are managed by Angular itself and can be accessed by a built-in Inversion of Control (IoC) container aka Dependency Injection. Beside those, there are different places that are not managed by Angular’s IoC. Namely, there are Custom Reactive Form Validators and Custom RxJS Operators
and more, but I’ll only touch on validators and operators.
Why? Validators and Operators are usually pure functions
or static functions
because they will be invoked by Reactive Form
and RxJS
respectively.
Because of that, we do not want to write these custom validators
and operators
with dependence to an instance
, aka this
. However without this
,
DI becomes tricky because you cannot use the common DI pattern anymore, or DI via constructor.
export class SomeService {
constructor(private readonly otherService: OtherService) {}
static someStaticFn() {
// you can't access this.otherService in here
}
}
As some of you might have known, providedIn: 'root'
is an extra option that you can pass in @Injectable
decorator to let
Angular knows that you want to provide this Injectable
in the root injector. Using providedIn: 'root'
will allow you
to have lazy-loaded singleton services.
Imagine you have a LogService
that will handle API errors and log the errors to a 3rd-party logging service, like ApplicationInsight.
Let’s take a look at the following pseudo-code
@Injectable({ providedIn: "root" })
export class LogService {
constructor(private readonly appInsight: ApplicationInsight) {}
log(error: any) {
this.appInsight.trackError(error);
}
}
To use LogService
:
// somewhere in the codebase
this.httpClient.get(someUrl).pipe(
catchError((err) => {
console.log(err);
this.logService.log(err);
return throwError(err);
})
);
The above works perfectly fine. But, you do not have ONE API call in the whole application. It is going to get very verbose
to apply that catchError()
to every single API calls.
Then you remember you can create Custom Operator to encapsulate reusable logic. You go ahead and start putting together the following operator:
export function logAndRethrowError<TInput = any>(): MonoTypeOperatorFunction<
TInput
> {
return catchError((err) => {
console.log(err);
// this.logService.log(err);
return throwError(err);
});
}
Now you realize that you need a LogService
instance, you start refactoring the operator
as follow:
@Injectable({ providedIn: 'root' })
export class OperatorUtil {
constructor(private readonly logService: LogService) {}
logAndRethrowError<TInput = any>()L MonoTypeOperatorFunction<TInput> {
return catchError(err => {
console.log(err);
this.logService.log(err);
return throwError(err);
});
}
}
Everything looks good now and you can start using your custom operator:
this.httpClient.get(someUrl).pipe(this.operatorUtil.logAndRethrowError());
Of course, you would need to inject
OperatorUtil
in wherever you’re callingthis.httpClient.get(...)
The above approach works fine but there are a couple of things:
OperatorUtil
as instance methodsOperatorUtil
everywhere you want to use those custom operatorsAs you can see, without custom operators being instance methods, there is no way you can access the this.logService
inside
of your custom operators. Is there a solution? Yes, there is.
This solution applies to Custom Validators as Custom Operators. And this solution only works with
providedIn: 'root'
injectables.
The solution is to create a class called RootInjector
(in fact, you can call it whatever you want) with some static properties and methods to
keep track of the root Injector that gets all the providedIn: 'root'
providers.
export class RootInjector {
private static rootInjector: Injector;
private static readonly $injectorReady = new BehaviorSubject(false);
readonly injectorReady$ = this.$injectorReady.asObservable();
static setInjector(injector: Injector) {
if (this.rootInjector) {
return;
}
this.rootInjector = injector;
this.$injectorReady.next(true);
}
static get<T>(
token: Type<T> | InjectionToken<T>,
notFoundValue?: T,
flags?: InjectFlags
): T {
try {
return this.rootInjector.get(token, notFoundValue, flags);
} catch (e) {
console.error(
`Error getting ${token} from RootInjector. This is likely due to RootInjector is undefined. Please check RootInjector.rootInjector value.`
);
return null;
}
}
}
The above is RootInjector
implementation. Now the question is “where to use it?” The answer is “main.ts”. In main.ts
(or your entry file), you’ll always
call bootstrapModule()
to start bootstraping your Angular application. bootstrapModule()
returns a Promise
with the ModuleRef
as the resolved value.
And there is a property called injector
on the ModuleRef
that is exactly the root injector that we’re interested in. To be more accurate, the injector
on ModuleRef
is whatever module’s injector we use to bootstrap. In most cases, it is AppModule
. So you can guarantee that the Injectables which are provided in
this injector
will be singletons throughout your application.
platformBrowserDynamic.bootstrapModule(AppModule).then((ngModuleRef) => {
RootInjector.setInjector(ngModuleRef.injector);
});
Now back to our logAndRethrowError()
operator, we can leverage RootInjector
to get the LogService
singleton (since LogService
is providedIn: 'root'
)
export function logAndRethrowError<TInput = any>(
beforeRethrow?: (err?: any) => void
): MonoTypeOperatorFunction<TInput> {
const logService = RootInjector.get(LogService);
return catchError((err) => {
console.log(err);
logService.log(err);
beforeRethrow?.(err);
return throwError(err);
});
}
I added a
beforeRethrow
callback in case you want to execute additional logic before rethrow happens.
Then, we can change our httpClient.get()
implementation a little bit to use logAndRethrowError()
this.httpClient.get(someUrl).pipe(logAndRethrowError());
// with callback
this.httpClient.get(someUrl).pipe(
logAndRethrowError((err) => {
this.toastService.error(err);
})
);
RootInjector
is a nice and elegant way of keeping track of the root injector so that you can access the Injectables
anywhere throughout your application. Although, there are gotchas. RootInjector
depends on the actual bootstrap process
of AppModule
and if you try to access RootInjector
before AppModule
has been bootstrapped, exceptions will be thrown.
What are such occasions? APP_INITIALIZER
and Guards
/Resolvers
/ErrorHandler
that might halt the bootstrap process.
RootInjector.injectorReady$
is a workaround for some cases where you want to use RootInjector
in AppComponent
to initialize
some data.
Hopefully, you learn something new today. Thanks for reading and I’ll see you soon 🤞
Written by Chau Tran