Using dependency injection functions instead of constructors

Introduction

Starting in Angular 14, we can use the new inject function in components, directives and pipes. There are several benefits in using this approach as it significantly improves the readability and reusability of our code.

For example, instead of:

export class MyComponent {
  constructor(private apiService: APIService) {}
}

we can just write:

export class MyComponent {
  private apiService = inject(APIService);
}

This approach is really useful for components with a large number of dependencies as it's far more readable to pass in dependencies in the definition of our class, rather than in the constructor.

So, we can change code like this:

export class MyComponent {
  constructor(
    private apiService: APIService,
    private router: Router,
    private activatedRoute: ActivatedRoute
  ) {}
}

to this:

export class MyComponent {
  private apiService = inject(APIService);
  private router = inject(Router);
  private activatedRoute = inject(ActivatedRoute);
}

Reusable functions

One of the main benefits of using the inject function is the ability to create reusable and modular functions.

An example of this is the code required to unsubscribe from observables when the component is destroyed.

Instead of writing code like this:

export class MyComponent {
  private destroy$ = new Subject();
  ngOnInit() {
    this.apiService.getData().pipe(
      takeUntil(this.destroy$)
    ).subscribe(data => console.log(data));
  }
  ngOnDestroy(): void {
    this.destroy$.next(true);
    this.destroy$.complete();
  }
}

we can simply write:

export class MyComponent {
  private destroy$ = untilDestroyed();
  ngOnInit() {
    this.apiService.getData().pipe(
      takeUntil(this.destroy$)
    ).subscribe(data => console.log(data));
  }
}

Instead of repeating the logic to unsubscribe in every component, we can simply create a reusable function to handle the unsubscribe logic:

export function untilDestroyed() {
  const subject = new Subject<void>();
  const viewRef = inject(ChangeDetectorRef) as ViewRef;
  viewRef.onDestroy(() => {
    subject.next();
    subject.complete()
  });
  return takeUntil(subject.asObservable());
}

Some other use cases include writing logic to retrieve a specific query parameter.

So, rather than writing:

export class MyComponent {
  id$ = this.activatedRoute.queryParams.pipe(
    map(params => params.id)
  );
  constructor(private activatedRoute: ActivatedRoute) {}
}

we can simply rewrite our component as:

export class MyComponent {
  id$ = getParam$('id');
}

The logic to determine the current query parameter can then be rewritten as a common function:

export function getParam$(key: string) {
  const activatedRoute = inject(ActivatedRoute);
  return activatedRoute.queryParams.pipe(
    map(params => params[key])
  );
}

This can prevent having to duplicate logic in multiple components.

Did you find this article valuable?

Support Alex Routledge by becoming a sponsor. Any amount is appreciated!