Getting Started

With the most recent release of Angular, dependency injection has become even more flexible.

Starting with version 14, injection is no longer limited to class constructors; it can be used outside of an injection context.

This shift opens up a wide range of opportunities, notably the ability to build composable utilities.

How does this work?
How can we adopt it in our projects?

The answers lie in the updated behavior of the inject function.

What Does the inject Function Do?

The inject function retrieves the reference of a given "token" from the currently active injector.

In previous Angular versions, this function was confined to injection contexts.

export const MY_HTTP = new InjectionToken('MY_HTTP', {
  provideIn: 'root',
  useFactory() {
    return inject(HttpClient);
  }
})

@Injectable()
export class TodoService {
  http = inject(MY_HTTP);
}
Enter fullscreen mode Exit fullscreen mode

What's New?

Starting with Angular version 14, the inject function can be invoked outside an injection context, provided it is called during:

  • the instantiation of a class
  • the initialization of a class parameter.

Simply put, this function can now be used in components, directives, and pipes.

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: './todo.component.less'
})
export class TodoComponent {
  todos$ = inject(TodoService).getTodos();
}
Enter fullscreen mode Exit fullscreen mode

Composition in Practice

This new pattern makes it remarkably straightforward to write highly reusable functions.

For developers familiar with Vue, this concept may feel similar to their composition API.

// params.helper.ts
export function getParam<T>(key: string): Observable<T> {
  const route = inject(ActivatedRoute);
  return route.paramMap.pipe(
    map(params => params.get(key)),
    distinctUntilChanged()
  );
}
Enter fullscreen mode Exit fullscreen mode
// todo-details.component.ts
@Component({
  selector: 'app-todo-details',
  templateUrl: './todo-details.component.html',
  styleUrls: './todo-details.component.less'
})
export class TodoComponent {
  todoId$ = getParam<Todo>('id');
  todo$ = todoId$.pipe(
    switchMap(id => inject(TodoService).getTodo(id))
  );
}
Enter fullscreen mode Exit fullscreen mode

Another highly practical use case is the automatic cleanup of observables when a component is destroyed.

Angular provides the OnDestroy hook, accessible via the ViewRef, to manage teardown logic.

This makes it straightforward to write generic logic for unsubscribing from observables.

// clean-observable.helper.ts
export function untilDestroyed() {
  const subject = new Subject<void>();

  const viewRef = inject(ChangeDetectorRef) as ViewRef;

  viewRef.onDestroy(() => {
    subject.next();
    subject.complete()
  });

  return takeUntil(subject.asObservable())
}
Enter fullscreen mode Exit fullscreen mode
// todo-details.component.ts
@Component({
  selector: 'app-todo-details',
  templateUrl: './todo-details.component.html',
  styleUrls: './todo-details.component.less'
})
export class TodoComponent {
  unsubscribe$ = untilDestroyed();
  refreshDetails$ = new Subject<void>();
  ngOnInit(): void {
    this.refreshDetails$.pipe(unsubscribe$).subscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

This new capability is undeniably powerful, offering nearly endless possibilities for reuse and composition—but it does come with trade-offs.

First, it can only be used while constructing components. This means that accessing a component's Input properties isn't possible. A workaround using closures exists, but it's not recommended.

Second, testing components becomes more challenging, as writing mocks will require more effort.