DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Always use "inject"
Armen Vardanyan for This is Angular

Posted on

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.

Top comments (5)

Collapse
 
bwca profile image
Volodymyr Yepishev

I would prefer to avoid long inheritance chains over sprinkling inject over them, though :)

Collapse
 
armandotrue profile image
Armen Vardanyan

Yeah sure inheriting components/directives is, in most cases, not the best idea. But sometimes components that do similar things in different contexts beg for using inheritance, so "inject" will reduce boilerplate code.

Collapse
 
oz profile image
Evgeniy OZ

I mostly agree and you've explained all the Pros correctly.

One thing I disagree about is: usage described in "Custom RxJS operators" is too fragile and creates tight coupling. Also, it might cause unwanted side effects and, therefore, hard-to-find bugs.

But, overall, I'm happy to celebrate our new tool in Angular, which is extremely helpful, and opens some new horizons for us ;)

Collapse
 
armandotrue profile image
Armen Vardanyan

While I agree mostly, I think in some cases it can be applicable, for example NgRx effect pipelines, or maybe using simple services that do not have side effects

Collapse
 
thavarajan profile image
Thavarajan

Are we lost the dependency tree ability, searching in the parents

11 Tips That Make You a Better Typescript Programmer

1 Think in {Set}

Type is an everyday concept to programmers, but it’s surprisingly difficult to define it succinctly. I find it helpful to use Set as a conceptual model instead.

#2 Understand declared type and narrowed type

One extremely powerful typescript feature is automatic type narrowing based on control flow. This means a variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type.

#3 Use discriminated union instead of optional fields

...

Read the whole post now!