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);
}
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();
}
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()
);
}
// 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))
);
}
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())
}
// 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();
}
}
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.
