The new way of subscribing in an Angular Component
Observables and RxJS are considered the cornerstones of the Angular framework. The RxJS library allows us to create our applications in a reactive way. Nevertheless, observables come with one pretty large downside. Every time you subscribe to an observable you have to remember that you must unsubscribe from it later. Otherwise, you will create a potential bug in your application or worse, a memory leak. Dealing with it and remembering about it every time you subscribe to an observable can be a nuisance.
Over the years developers have created countless methods of managing subscriptions. Still, the community hasn’t agreed on one unified solution. I’m a big fan of choosing one practice and sticking to it, so the application looks like it has been written by one developer and the code is consistent throughout the project.
Angular version 14 recently came out and it introduced a couple of new features. One of them allowed creating a new way of subscription management. In this article, I would like to show you this method called ReactiveContext.
The complete implementation can be found in the Github repository Functional Angular.
Subscription management
Let’s take a look at the example of observable usage and the most common subscription management technique in a component:
As you can see in the component we are subscribing to a stream of click events. In order to do it properly, we need to unsubscribe from the stream when the component is destroyed. Otherwise, we will create a memory leak or a potential bug. The most common approach is the “takeUntil” method which requires to:
- create a Subject and assign it to the property unsubscribe$,
- implement the ngOnDestroy method that next value and completes the unsubscribe$ Subject,
- add takeUntil operator before the subscribe is invoked.
If you think about it that is kind of a lot of work. That is why we have already so many libraries for subscription management and why trying to find the best solution is so important.
ReactiveContext
Now let’s take a look at what the previous example looks like when using the ReactiveContext method:
All you have to do is to wrap your observable in a useReactiveContext function. It will automatically unsubscribe from an observable when the component is destroyed.
In comparison to the previous “takeUntil” method, you don’t have to:
- remember about creating a dedicated Subject and implement ngOnDestroy method,
- use the
takeUntil
operator.
Implementation
The useReactiveContext
function wraps your observable in a context and allows you to forget about subscription management. You can easily just subscribe to your stream and don’t think about what will happen when a component is destroyed. Let’s take a look at the basic implementation to see how it works under the hood:
export function useReactiveContext<T>(stream$: Observable<T>) {
const changeDetector = inject(ChangeDetectorRef),
unsubscribe$ = new Subject<void>();
(changeDetector as ViewRef).onDestroy(() => {
unsubscribe$.next();
unsubscribe$.complete();
});
return {
subscribe(
next?: (value: T) => void,
error?: (e: any) => void,
complete?: () => void
): Subscription {
return stream$
.pipe(takeUntil(unsubscribe$))
.subscribe(next, error, complete);
},
};
}
Basically, the useReactiveContext
function takes an observable as an argument and wraps it with the takeUntil
operator. Since Angular 14 ChangeDetectorRef
allows accessing a new method onDestroy
that lets you to run your code when a component is destroyed. In the method above we have leveraged that fact and used it to automatically clean the subscription. The useReactiveContext
function creates a reference to the ChangeDetectorRef
using the new inject function. This way you don’t have to pass a reference to the ChangeDetectorRef
as a function argument.
Unfortunately, the inject function comes with some downsides. It can be used only in a constructor or on class properties. That means the useReactiveContext
function can also only be used in these exact places. I will show you a workaround in the next part of this article.
Outside of constructor
The inject mechanism narrows the places where the useReactiveContext
function can be used. Fortunately, there are two workarounds that omit this issue.
Assign ReactiveContext to a class property
You can create a class property that holds a reference to the context created by the useReactiveContext
function. That solution allows you to use reactive context in class methods outside of the constructor scope:
In order for this to work you need to add a connect
method that connects observable with a reactive context:
export function useReactiveContext<T>(stream$: Observable<T>) {
const changeDetector = inject(ChangeDetectorRef),
unsubscribe$ = new Subject<void>(),
innerStream$ = stream$;
let innerStream$: Observable<T> | undefined;
if (stream$) {
innerStream$ = stream$.pipe(takeUntil(unsub$));
}
(changeDetector as ViewRef).onDestroy(() => {
unsubscribe$.next();
unsubscribe$.complete();
});
return {
connect: (stream$: Observable<T>) => {
innerStream$ = stream$.pipe(takeUntil(unsub$));
return context;
},
subscribe(
next?: (value: T) => void,
error?: (e: any) => void,
complete?: () => void
): Subscription {
return innerStream$
.pipe(takeUntil(unsubscribe$))
.subscribe(next, error, complete);
},
};
}
Manually pass a reference to the ChangeDetectorRef
Another solution is to pass a ChangeDetectorRef
as an argument of the useReactiveContext
:
This way we don’t have to rely on the inject function, so there are no downsides to reducing the scope of places where it can be used:
export function useReactiveContext<T>(
stream$: Observable<T>,
cd?: ChangeDetectorRef
) {
const changeDetector = cd ? cd : inject(ChangeDetectorRef),
unsubscribe$ = new Subject<void>(),
innerStream$ = stream$;
let innerStream$: Observable<T> | undefined;
if (stream$) {
innerStream$ = stream$.pipe(takeUntil(unsubscribe$));
}
(changeDetector as ViewRef).onDestroy(() => {
unsubscribe$.next();
unsubscribe$.complete();
});
const context = {
connect: (stream$: Observable<T>) => {
innerStream$ = stream$.pipe(takeUntil(unsubscribe$));
return context;
},
subscribe(
next?: (value: T) => void,
error?: (e: any) => void,
complete?: () => void
): Subscription {
return innerStream$
.pipe(takeUntil(unsubscribe$))
.subscribe(next, error, complete);
},
};
return context;
}
Observable in the template
So far we have discussed examples that strictly focus on using observables with browser events. Observables are commonly used with a template of a component. The most popular solution for that is to use:
- async pipe — which I'm not a fan of, causes of bad performance,
- rx-angular — rxLet, rxFor, rxIf directives solve async pipe issues and are the recommended way of subscribing to an observable in a component template.
Apart from the above-mentioned solutions, there are some other cases where you have to subscribe to an observable in a component and run detect changes manually:
This code can be simplified by using the useReactiveContext
function:
The useReactiveContext
already uses ChangeDetectorRef
so we can take advantage of that and use it to manually trigger detectChanges
when value appears in an observable. Instead of using subscribe use subscribeAndRender
:
export function useReactiveContext<T>(stream$: Observable<T>) {
const changeDetector = inject(ChangeDetectorRef),
unsubscribe$ = new Subject<void>();
// ...
return {
// ...
subscribeAndRender(
next?: (value: T) => void,
error?: (e: any) => void,
complete?: () => void
): Subscription {
return innerStream$.subscribe(
(v) => {
next(v);
changeDetector.detectChanges();
},
error,
complete
);
},
};
}
This solution works perfectly in an application that uses ChangeDetectionStrategy.onPush
or has ngZones disabled.
Conclusions
The reactive context is a new way of dealing with subscriptions in a component. It uses mechanics that were implemented in the latest release of Angular version 14. The useReactiveContext
function has a clean and flexible API. As with every solution, it comes with some pros and cons.
Pros
- automatic unsubscribe, don't have to remember about the takeUntil operator,
- it uses the functional approach - it's easy to use and read,
- no boilerplate - in comparison to other solutions,
- it offers great performance,
- works with the onPush strategy, as well it can be used in a zone less applications.
Cons
- using
reactiveContext
outside of constructor or class properties requires passing reference to theChangeDetectorRef
, - can only be used in a Component, Directive or Pipe,
- unfortunately, this solution doesn't work when a component uses a
@ViewChild
mechanism(hope that the Angular team will fix this in the future).
Disclaimer
The solution presented in this article is an experimental approach as it uses the onDestroy
method from the ViewRef
. It is used to unsubscribe from all observables when an instance of a component is destroyed. The onDestroy
method is not available on the ChangeDetectorRef
instead, type projection has to be used:
Functional Angular Repository
The complete implementation can be found in the Github repository Functional Angular.
Final thoughts
When it comes to subscription management Angular community cannot agree on one final method. The latest versions of Angular introduce new features that inspire us to find better solutions for common issues. I really suggest checking out this new possibility of subscribing to an observable in a component.
Subscription management is a nuisance when it comes to using RxJS with Angular. It is hard to imagine how much time developers spend on making sure their applications are memory leak free. I hope in the future we will have one commonly used mechanism and won't have to bother ourselves with RxJS subscriptions.