Always use "inject"
Armen Vardanyan
Posted on December 2, 2022
Original cover photo by Markus Spiske on Unsplash.
Introduction
Starting from Angular version 14, a function named "inject" became available for developers to use. This function is used to inject dependencies into injection contexts, meaning anything that is used inside components, directives, and so on: whenever dependency injection via a constructor is available. This allows the developers to write functions that can both be reused by components and use dependency injection. For example, we can use references to the router data in functions to simplify sharing data:
import { ActivatedRoute } from '@angular/router';
function getRouteParam(paramName: string): string {
const route = inject(ActivatedRoute);
return route.snapshot.paramMap.get(paramName);
}
@Component({
selector: 'app-root',
template: `
<h1>Route param: {{ id }}</h1>
`,
})
export class AppComponent {
id = getRouteParam('id');
}
As you can see, we now can use this function to access route parameters anywhere without the need to inject the ActivatedRoute
into the components everytime.
Of course, this is a powerful tool, but with its arrival, a question arises:
Should we ditch the constructor and use
inject
everywhere?
So let's discuss pros and cons.
Pros
1. Reusability
As we've seen, we can use inject
to share code between components. This is a huge advantage, as we can now write functions that can be used in multiple components without the need to inject the same dependencies over and over again. This is especially useful when we have a lot of components that use the same dependencies. Now, if we have a piece of reusable logic that relies on some injection token (a service, a value or something else), we are no longer restrained to using classes; we can just write functions, which are arguably a simpler and more flexible way to write code. (this is an opinion).
2. Type inference
Previously, when we created a class that used some injection token, we had to explicitly define the type of the property that would hold the injected value. This is no longer necessary, as the type of the injected value is inferred from the type of the injection token. This is especially useful when we utilize the InjectionToken
class, which we can now use without the @Inject
decorator. Here, take this as an example:
import { InjectionToken } from '@angular/core';
const TOKEN = new InjectionToken<string>('token');
@Component({
// component metadata
})
export class AppComponent {
constructor(
@Inject(TOKEN) token: string,
// we had to explicitly define
// the type of the token property
) {
// ...
}
}
This is both more verbose and depends on what we put as the type of token
explicitly. Now, we can just write:
import { InjectionToken } from '@angular/core';
const TOKEN = new InjectionToken<string>('token');
@Component({
// component metadata
})
export class AppComponent {
private token = inject(TOKEN);
// type "string" is inferred
}
Of course, the same type inference works on Services, and pretty much anything that can be injected.
3. Easier inheritance
Estending Angular components/directives and so on from other classes has always been kinda painful, especially if we use injected dependencies. Problem is that with constructor injection we have to pass the dependencies to the parent constructor, which requires writing repetitive code and becomes the harder the longer the inheritance chain. Take a look:
export class ParentClass {
constructor(
private router: Router,
) {
// ...
}
}
@Component({
// component metadata
})
export class ChildComponent extends ParentClass {
constructor(
// we have to inject all the
// parent dependencies again
// and add others
private router: Router,
private http: HttpClient,
) {
super(router); // also pass to the parent
}
}
With inject, we can essentially skip the constructors whatsoever, and just use the inject
function to get the dependencies. This is especially useful when we have a long inheritance chain, as we can just use inject
to get the dependencies we need, and don't have to worry about passing them to the parent constructor. Here's how it looks:
export class ParentClass {
private router = inject(Router);
}
@Component({
// component metadata
})
export class ChildComponent extends ParentClass {
private http = inject(HttpClient);
}
Let's now get to the final advantage usinjg inject
brings:
4. Custom RxJS operators
With inject
, we can create custom RxJS operators that use dependency injection. This is especially useful when we want to create a custom operator that uses a service, but we don't want to inject the service into the component and pass the reference to the operator. Here's an example without inject
:
function toFormData(utilitiesService: UtilitiesService) {
return (source: Observable<any>) => {
return source.pipe(
map((value) => {
return utilitiesService.toFormData(value);
}),
);
};
}
@Component({
// component metadata
})
export class AppComponent {
constructor(
private utilitiesService: UtilitiesService,
) {
// ...
}
private formData$ = this.http.get('https://example.com').pipe(
toFormData(this.utilitiesService),
);
}
Now, passing a service each time we want to use this operator will become tedious, especially when the operator also requires other arguments. Now, let's see how it looks with inject
:
function toFormData() {
const utilitiesService = inject(UtilitiesService);
return (source: Observable<any>) => {
return source.pipe(
map((value) => {
return utilitiesService.toFormData(value);
}),
);
};
}
@Component({
// component metadata
})
export class AppComponent {
private formData$ = this.http.get('https://example.com').pipe(
toFormData(),
);
}
Nice and clean! Extendability of RxJS signifantly increases with this approach.
Cons
Downsides to this approach are mostly minimal, but still worth mentioning.
1. Unfamiliarity
inject
only recently became availsble as an API you can import and use, and it does not really have analogs in any other framework, so it might be unfamiliar to some developers. This is not a big deal, as it's easy to learn, and won't be a problem in the future.
2. Availability
The function is only available in dependency injection contexts, so trying to use it in model or DTO classes, for example, will result in errors. Read more about this in the Angular documentation. This has a workaround using the runInContext
API, with something like this:
@Component({
// component metadata
})
export class AppComponent {
constructor(
private injector: EnvironmentInjector,
) {}
ngOnInit() {
this.injector.runInContext(() => {
const token = inject(TOKEN);
// use the token freely outside of the constructor
});
}
Read more about this in an article by Nethanel Basal: Getting to Know the runInContext API in Angular.
3. Testing
Probably the biggest downside to this approach is that it makes testing a bit harder. If you have been creating instances of services for testing purposes using the new
keyword to bypass using TestBed
, you can't do it if the services use inject
, making us bound to TestBed
Conclusion
With each version, Angular provides developers with more and more different features and new approaches to solve problems. Hopefully, this article will help you to understand the new inject
function and how to use it in your projects.
Posted on December 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
July 16, 2024