Since Angular 14, we have had the almighty inject()
function as a way to inject dependencies into our Angular entities.
We wonβt be discussing the differences between
inject()
and traditional Constructor DI in this blog post.
Custom Inject Functions
Some folks are going to hate me for this π
, but itβs ok. I personally like inject()
because it allows for better compositions with Custom Inject Functions (if you are familiar with React, then this is somewhat similar to Custom Hooks). However, there is one caveat to inject()
that it has to be invoked in an Injection Context.
The snippets in this blog post will be using some of Angular Three so it makes sense for some of the points I am going to make.
It is easy to spot misuses of inject()
when we use it directly like follow:
On the other hand, errors relating to Injection Context are harder to spot (and debug) when we have Custom Inject Functions (CIFs). Letβs assume that we want to provide an easier way for consumers to run some code in the animation loop, we will probably need to create a CIF
Then, our component can be updated as follow π!
The Limitations
This looks clean! But, injectBeforeRender
comes with some limitations. Letβs take a look at the following scenario
Limitation 1: Input values arenβt resolved in Injection Context, yet
We now have to pass renderPriority
in injectBeforeRender
as the second argument. Of course, we can invoke injectBeforeRender
in constructor
but by the time the constructor
is invoked, Angular hasnβt resolved the Input value yet.
Limitation 2: Outside of Injection Context
We know that ngOnInit
is one of the places where Angular has resolved the Input value but ngOnInit
is invoked outside of an Injection Context.
One extra caveat for the 2nd limitation is when injectBeforeRender
throws, we will see a generic message relating to inject()
being invoked outside of an Injection Context. Nothing points to injectBeforeRender
being the one function that throws.
We cannot fix the limitations but we can at least workaround them by making our CIF more robust and more responsible. Yes, for the extra caveat as well.
The better way of making a CIF
First, letβs work on the extra caveat. This one is easy because we can use a utility provided by Angular assertInInjectionContext()
And with that, the extra caveat is taken care of. When injectBeforeRender
throws (in dev mode), we will see an error stating that injectBeforeRender
being invoked outside of an Injection Context.
To work around the limitations, we need to allow our CIF to accept an optional parameter of type Injector
. An Injector
represents the Injection Context that provides that Injector
. With the Injector
argument, the consumers can control the Injection Context that a CIF is invoked. We want it to be optional because most of the times, it should not be needed.
Half way there! Our CIF
now has injector
argument but it has to decide whether to use that custom injector or use the default injector (i.e: the current Injection Context that the CIF is invoked in). To achieve this, we will create a function that will guarantee anything below it is running in an Injection Context
With this, we can update our CIF as follow
Why do we use runInInjectionContext
?
As its name suggests, runInInjectionContext
runs arbitrary code in a provided Injector Context (i.e: an Injector
). Instead of runInInjectionContext
, we can also use injector.get()
to retrieve the dependencies that our CIF needs but injector.get()
seems like Service Locator which is seen as an anti-pattern by many.
Additionally, refactoring code to use runInInjectionContext
is easy because we can move our existing code inside of runInInjectionContext
and everything goes back to working.
How do we consume our CIF now?
With the above changes, consumers can safely consume our CIF injectBeforeRender
in many different ways
Conclusion
With the help of assertInInjectionContext
and runInInjectionContext
, weβve made our Custom Inject Function (CIF) more robust by allowing the consumers to control the Injection Context that the CIF is invoked in and more responsible by telling the consumers that our CIF is the one throwing error if it is invoked outside of an Injection Context.
I personally use this approach for all CIFs that angular-three
has. We did not discuss Testing CIFs in this blog post but Iβll definitely write up a new one when I discover things to share in that regard. For now, have fun!
Thank you
Thanks Enea for reviewing!