Always use "inject"

armandotrue

Armen Vardanyan

Posted on December 2, 2022

Always use "inject"

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');
}
Enter fullscreen mode Exit fullscreen mode

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
    ) {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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),
    );
}
Enter fullscreen mode Exit fullscreen mode

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(),
    );
}
Enter fullscreen mode Exit fullscreen mode

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
       });
   }
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
armandotrue
Armen Vardanyan

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