Up until recently, Angular has been a hard-core class-based framework: Component, Directive, Pipe, Guard, Interceptor, Service, etc… Everything has been a TypeScript class. Nowadays, Angular provides more functional APIs: Functional Guard, Functional Interceptor, and Functional Resolver.
These functional APIs are great and if you haven’t tried them out, I’d highly recommend that you do because they do improve the authoring experience.
Now, let’s take a quick look at Component, Directive, and Pipe
All three of these building blocks have their dedicated Decorators, which means that we do not have much choice than to use classes for Component, Directive, and Pipe. What’s left?
Service
Yes, we have Service, the lord of general usage. If you haven’t noticed, Guard, Interceptor, and Resolver are all Services. What Angular developers are so fixtated on is that Service has to be a TypeScript class. Functional Guard/Resolver/Interceptor prove that to be not the case. So, for general-usage Service, what can we do today?
Let’s say we have a GithubUserService
, that is providedIn: 'root'
, with the following requirements:
- It needs to inject
HttpClient
to make network calls - It needs to expose a method
searchUser(query?: string)
to search for Github users based on a query
Any Angular developer should be able to write this Service in a heartbeat
Everything about this GithubUserService
is completely fine. Some folks might complain a bit about Testability in terms of
Services and inject()
. I too would prefer the ability to test Services with new ()
syntax but that is a topic for another day.
Next, I’d like to introduce an alternative to writing Services in Angular.
Injection Token
Many Angular developers, including Senior ones, are not familiar with InjectionToken. Even if they know InjectionToken
, a lot of developers do not utilize them enough. For this blog post, I’ll be using InjectionToken
to write a concise and easy to test Service
To spare me the heated arguments, I want to emphasize that this idea is purely exploratory at this point.
Let’s rewrite GithubUserService
using InjectionToken
. For this, we’ll have two separate units: a Factory Function and an Injection Token
Imagine our file structure is as follow:
We can choose to expose the Injection Token for consumers and keep the implementation (Factory Function) as private API to github-user
library.
This argument makes more sense for folks who work in a monorepo setup where each specific piece of functionality is a library that exposes its Public API for the rest of the monorepo.
Testing
As far as testing goes, we only care about testing our implementation details, githubUserServiceFactory
, which is just a function.
Pretty cool right? We can still use inject()
and keep tests as simple as possible.
Atomic Token
Previously, we implemented GithubUserService
as an InjectionToken
with a factory function. The return value of our factory is to mimic that of a class-based service. What if we only ever need searchUser()
method? Luckily, using InjectionToken
provides us this flexibility to return what we actually need.
Our
github-user.token.ts
stays mostly the same, with some name changes because we change our factory function name
When we use this in a Component, our code looks like the following:
This approach also allows us to separate tests, separate implementation details with different arguments. All functions do not necessarily share all of the Dependencies
Life-cycle hooks
As of this moment, the only life-cycle hook that we care about for Services is ngOnDestroy
. From Angular 16, we have a new token, DestroyRef, as a way to implement a clean-up mechanism for our Service. Let’s rewrite our factory to also expose more than just a searchUser
function
With this, let’s assume that our Service is no longer a Root Service so we need to rewrite the Token
We’re ready to use this in ANY Component that needs GithubUserServiceApi
Dealing with ComponentStore/RxState-like API
One limitation with this approach is how we can leverage APIs like ComponentStore or RxState.
Usually, the way to consume these APIs is to extends ComponentStore<>
or extends RxState<>
. However, we cannot extends
because we have no class. The only thing that we care about when extends ComponentStore<>
is for the Component Store to automatically run its destroy logic. Once again, DestroyRef
to the rescue
The same can be applied to
RxState
Using provider
Alternatively, we can provide then inject ComponentStore
instead of new ComponentStore()
since some library author might expose their API as an Abstract Class, or you simply do not like calling new
or ngOnDestroy()
manually
INITIAL_STATE_TOKEN
is imported from@ngrx/component-store
Possibilities
This approach opens up new possibility: configurable component store. Let’s say we want to reuse our UserStoreApi
but we want the consumers to have the ability to configure its initial state.
We definitely can expose setState
on UserStoreApi
and call setState
where we use UserStoreApi
to set the initial state. The limitation to this is we cannot call setState
if we are to provide UserStoreApi
on the Route-provider level.
On the other hand, we can turn our userStoreFactory
into a higher-order function to accept some initial state.
Now, we can provide different initial UserState
when we call provideUserStore()
; on Component-provider level, or on Route-provider level.
The same can be applied when use
provider
Conclusion
We briefly went over the current APIs on Angular’s building blocks and learned that some of the Angular APIs have gone away from Class-based approach. We also explored a new approach to writing Services using InjectionToken
. Hopefully, I’m able to express my thoughts on this new approach and you learn something from this post whether or not you agree with me. Thank you for reading.
Updates
Nov 24 2023
I have been using this approach for the past 6 months or so and it works great for my libraries. Since I used it so much, I’ve created a utility available in ngxtension
called createInjectionToken
FAQs
1. What is the practicality of this approach?
Good question and I’ll be honest. Nothing presented here have made it to any enterprise applications that I’m a part of. That said, I do use the approach in my side projects.
2. I like it, but is it too verbose to write?
Yes, it is a bit verbose. We can always abstract the creation of the Injection Token and the Factory Function to a utility function. Something like the following:
Here’s a Github Gist for one implementation of such utility.
3. What can we use if we don’t have DestroyRef
?
It is a unfortunate because DestroyRef
really does help. In older versions, you can maybe try the following hack:
4. Is InjectionToken
not a Singleton ?
Yes, by default, new InjectionToken('description')
is just a token that you need to provide something for it before you can inject it. However, new InjectionToken('description', {factory: () => {}})
is providedIn: 'root'
by default. Hence, the approach introduced here does give you a Singleton. However, this is all configurable per situation.
5. Application code vs Library code?
I did not think about this when I write the blog post but I think I subconsciously lean towards Library Author.
Special Thanks
This blog post is a bit in the exploratory space so I asked several of my friends in the community to review