How to Avoid Observables in Angular

Michael Hladky - Oct 16 '19 - - Dev Community

Angular is an object-oriented framework.
Even if there are a lot of things imperatively implemented, some services, and therefore also some third party libs, are reactive.
This is great because it provides both approaches in one framework, which is at the moment a more or less unique thing.

As reactive programming is hard for an imperative thinking mind, many people try to avoid it.

This article will help us to understand how to avoid it and also, we will learn where it makes sense to use observables.

As good example where reactive programming becomes easy
📦 @rx-angular/state should be named.
It's imperative functions set and get allow programmers to code in there used style while still get better performance.


Table of Content


TL;DR

If you DON'T want to use a reactive approach in your component you
should take the observable you want to get rid of, as soon as possible and do the following things:

  • subscribe to a stream and assign incoming values to a component property
  • if necessary, unsubscribe the stream as soon as the component gets destroyed

Minimal Information about RxJS

Observables are a unified API for pull- and push-based collections,

that can be composed in a functional way

As this sentence is maybe not trivial to understand let me split it into two pieces, unified API and functional composition.

I'll give a bit more information to both of them and compare them with an imperative approach.

Unified API:

Think about all the different APIs in the browser for asynchronous operations:

  • setInterval and clearInterval
  • addEventListener and removeEventListener
  • new Promise and [no dispose logic implemented]
  • requestAnimationFrame and cancelAnimationFrame
  • async and await

These are just some of them and we can already see they are all implemented differently.
Some of them, i.e. promises, are not even disposable at all.

RxJS wraps all of them and provides the following API:

  • subscribe and unsubscribe

This is meant by "a unified API".

Functional Composition:
To give an example, let's combine the items of two arrays into a new one.
The imperative approach looks like that:



    const arr1 = [1,2,3], arr2 = [4,5,6];
    let arr3 = [];
    for (let i of arr1) {
      arr3.push(i);
    }
    for (let i of arr2) {
      arr3.push(i);
    }


Enter fullscreen mode Exit fullscreen mode

We mutate the arr3 and push all items from arr1 and arr2 into arr3 by using the for ... of statement.

The functional approach looks like that:



const arr1 = [1,2,3], arr2 = [4,5,6];
const arr3 = arr1.concat(arr2);


Enter fullscreen mode Exit fullscreen mode

Here we create a new instance on an array that is a result of the concat array first-class function.

This is meant by "functional composition".


In the following article, we will learn
how to work with all the different APIs of the browser instead of the unified API of RxJS.
We will also see how to mutate state and leverage imperative programming instead of functional composition.

To elaborate with some more practical things we start with a part of Angular that provides reactivity and try to avoid it.

Comparing Basic Usecases

In this section, we will get a good overview of some of the scenarios we get in touch with reactive programming in Angular.

We will take a look at:

  • Reactive services provided by Angular
  • Cold and Hot Observables
  • Subscription handling

And see the reactive and imperative approach in comparison.

Retrieving values from single-shot observables

Let's solve a very primitive example first.
Retrieving data over HTTP and rendering it.

We start with the reactive approach and then try to convert it into an imperative approach.

Leveraging Reactive Programming (🎮 demo)



import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Component({
  selector: 'example1-rx',
  template: `
  <h2>Example1 - Leverage Reactive Programming</h2>
  Http result: {{result | async}}
  `
})
export class Example1RxComponent {
  result = this.http.get('https://api.github.com/users/ReactiveX')
      .pipe(map((user: any) => user.login));

  constructor(private http: HttpClient) {

  }

}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • subscribing to http.get by using the async pipe triggers:
    • an HTTP get request fires
    • we retrieve the result in the pipe and render it

On the next change detection run, we will see the latest emitted value in the view.

As observables from HttpClient are single-shot observables (like promises they complete after the first emission) we don't need to care about subscription handling here.

Avoid Reactive Programming (🎮 demo)



import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'example1-im',
  template: `
  <h2>Example1 - Avoid Reactive Programming</h2>
  Http result: {{result}}
  `
})
export class Example1ImComponent {
  result;

  constructor(private http: HttpClient) {
    this.result = this.http.get('https://api.github.com/users/ReactiveX')
      .subscribe((user: any) => this.result = user.login);
  }

}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • subscribing to http.get in the constructor triggers:
    • an HTTP get request fires
    • we retrieve the result in subscribe function

On the next change detection run, we will see the result in the view.

As observables from HttpClient are single-shot observables we don't need to care about subscription handling.

Retrieving values from on-going observables provided by an Angular service

Next, let's use an on-going observable provided by Angular service, the ActivatedRoute service.

Let's retrieve the route params, plucking out a single key and displaying its value in the view.

Again we start with the reactive approach first.

Leveraging Reactive Programming (🎮 demo)



import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';

@Component({
  selector: 'example2-rx',
  template: `
  <h2>Example2 - Leverage Reactive Programming</h2>
  URL param: {{page | async}}
  `
})
export class Example2RxComponent {
  page = this.route.params
      .pipe(map((params: any) => params.page));

  constructor(private route: ActivatedRoute) {
  }

}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • retrieving the new route params by using the async
  • deriving the values from the page param from params with a transformation operation using the map operator
  • by using the async pipe we:
    • subscribe to the observable on AfterContentChecked
    • applying the internal value to the next pipe return value

On the next change detection run, we will see the latest emitted value in the view.

If the component gets destroyed,
the subscription that got set up in the async pipe after the first run of AfterContentChecked
gets destroyed on the pipes ngOnDestroy 💾 hook.

There is no manual subscription handling necessary.

Avoiding Reactive Programming (🎮 demo)



import { Component} from '@angular/core';
import { ActivatedRoute} from '@angular/router';

@Component({
  selector: 'example2-im',
  template: `
  <h2>Example2 - Avoid Reactive Programming</h2>
  URL param: {{page}}
  `
})
export class Example2ImComponent {
  page;

  constructor(private route: ActivatedRoute) {
    this.route.params
      .subscribe(params => this.page = params.page)
  }

}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • retrieving the new route params by subscribing in the constructor
  • deriving the values from the page param from params object directly

On the next change detection run, we will see the latest emitted value in the view.

Even if the params observables from ActivatedRoute are on-going we don't care about subscription handling here.
Angular internally manages the Observable and it gets closed on ngOnDestroy of the ActivatedRoute.

Retrieving values from on-going observables provided by third-party libs

In this section, we take a look at a scenario not managed by the framework.
For this example, I will use the @ngrx/store library and it's Store service.

Retrieving state from the store and display its value in the view.

Leveraging Reactive Programming (🎮 demo)



import { Component } from '@angular/core';
import { Store } from '@ngrx/store';


@Component({
  selector: 'example3-rx',
  template: `
  <h2>Example3 - Leverage Reactive Programming</h2>
  Store value {{page | async}}
  `
})
export class Example3RxComponent {
  page = this.store.select(s => s.page);

  constructor(private store: Store<any>) {
  }

}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • retrieving the new state by using the async pipe
  • deriving the values from the page param from this.store by using the select method
  • by using the async pipe we:
    • subscribe to the observable on AfterContentChecked
    • apply the internal value to the next pipe return value

On the next change detection run, we will see the latest emitted value in the view.

If the component gets destroyed Angular manages the subscription over the async pipe.

Avoiding Reactive Programming (🎮 demo)



import { Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';

@Component({
  selector: 'example3-im',
  template: `
  <h2>Example3 - Avoid Reactive Programming</h2>
  Store value {{page}}
  `
})
export class Example3ImComponent implements OnDestroy {
  subscription;
  page;

  constructor(private store: Store<any>) {
    this.subscription = this.store.select(s => s.page)
    .subscribe(page => this.page = page);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • retrieving the new state by subscribing in the constructor
  • deriving the values from the page param from this.store by using the select method
  • we store the returned subscription from the subscribe call under subscription

On the next change detection run, we will see the latest emitted value in the view.

Here we have to manage the subscription in case the component gets destroyed.

  • when the component gets destroyed
    • we call this.subscription.unsubscribe() in the ngOnDestroy life-cycle hook.

Patterns to avoid observables

As these examples are very simple let me summarise the learning with a broader view.

Where to subscribe

We saw that we subscribe to observables in different places.

Let's get a quick overview of the different options where we could subscribe.

  1. constructor
  2. ngOnChanges
  3. ngOnInit
  4. ngAfterContentInit
  5. ngAfterContentChecked
  6. subscription over async pipe with a template binding
  7. subscription over async pipe with a template expression
  8. ngAfterViewInit
  9. ngAfterViewChecked
  10. ngOnDestroy

If we take another look at the above code examples we realize that we put our subscription in the constructor to avoid reactive programming.
And we put the subscription in the template when we leveraged reactive programming.

This is the critical thing, the subscription.
The subscription is the place where values are "dripping" out of the observable.
It's the moment we start to mutate the properties of a component in an imperative way.

In RxJS .subscribe() is where reactive programming ends

So the worst thing you could do to avoid reactive programming is to use the async pipe.
Let me give you a quick illustration of this learning:

We learned the following:

  • If we want to avoid reactive programming we have to subscribe as early as possible, i. e. in the constructor.
  • If we want to leverage reactive programming we have to subscribe as late as possible, i. e. in the template.

As the last thing to mention here is a thing that I discover a lot when I consult projects is mixing the styles.
Until now I saw plenty of them and it was always a mess.

So as a suggestion from my side tries to avoid mixing styles as good as possible.

Make it even easier

We realized that there is a bit of boilerplate to write to get the values out of the observable.
In some cases, we also need to manage the subscription according to the component's lifetime.

As this is annoying or even tricky, if we forget to unsubscribe, we could create some helper functions to do so.

We could... But let's first look at some solutions out there.

In recent times 2 people presented automation of something that I call "binding an observable to a property" for Angular components.
Both of them created a HOC for it in a different way. (HOC is an acronym and stands for Higher Order Components)

@EliranEliassy presented the "@Unsubscriber" decorator in his presentation 📼 Everything you need to know about Ivy

@MikeRyanDev presented the "ReactiveComponent" and its "connect" method in his presentation 📼 Building with Ivy: rethinking reactive Angular

Both of them eliminate the need to assign incoming values to a component property as well as manage the subscription.
The great thing about it is that we can solve our problem with a one-liner and can switch to imperative programming without having any troubles.

That functionality could be implemented in various ways.

Eliran Eliassy used class-level decorators to accomplish it.
He uses a concept from functional programming, a closure function, that looks something like this:



import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Unsubscriber } from 'unsubscriber.decorator.ts';

@Unsubscriber()
@Component({
  selector: 'comp',
  template: `
  <h2>@Unsubscriber</h2>
  Store value {{page}}
  `
})
export class ExampleComponent {
  page; 
  subscription = this.store.select(s => s.page)
    .subscribe(page => this.page = page);

  constructor(private store: Store<any>) {
  }
}


Enter fullscreen mode Exit fullscreen mode

Now let's take a look at Mike Ryan’s example:

Mike used inheritance as an implementation approach. He also listed the various ways of implementation in his talk, so you should definitely watch it!
In addition to Eliran's example here the subscription call is also invisible, which is even better!
This is also the cleanest solution I have found so far.

He used a concept form object oriented programming, inheritance, that looks something like this:



import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { ReactiveComponent } from 'reactive.component.ts';
@Component({
  selector: 'comp',
  template: `
  <h2>ReactiveComponent</h2>
  Store value {{state.page}}
  `
})
export class ExampleComponent extends ReactiveComponent {
  state = this.connect({page: this.store.select(s => s.page)});

  constructor(private store: Store<any>) {
  }
}


Enter fullscreen mode Exit fullscreen mode

As both versions are written for Ivy you might wonder how to use it right now?
I can inform you that there are also several other libs out there for ViewEngine.

There is i.e. 📦 ngx-take-until-destroy and 📦 ngx-auto-unsubscribe from @NetanelBasal


With this information, we could stop here and start avoiding reactive programming like a pro. ;)

But let's have the last section before we make our conclusions.

Composing asynchronous processes

In this section, we will compose values from the Store with results from HTTP requests and render it in the template.
As we want to avoid broken UI state we have to handle race-conditions.
Even if there is no user interaction we refresh the result every 10 seconds automatically.
Also, if the component gets destroyed while a request is pending we don't process the result anymore.

As I mentioned that it maybe makes no sense to have the HTTP request as an observable I will use the browser’s fetch API instead of HTTPClient to fire the HTTP request.

Leveraging Reactive Programming (🎮 demo)



import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { merge, interval } from 'rxjs';
import { map, switchMap, withLatestFrom } from 'rxjs/operators';
import { Store } from '@ngrx/store';

@Component({
  selector: 'example4-rx',
  template: `
  <h2>Example4 - Leverage Reactive Programming</h2>
  Repositories Page [{{page | async}}]: 
  <ul>
  <li *ngFor="let name of names | async">{{name}}</li>
  </ul>
  `
})
export class Example4RxComponent {
  page = this.store.select(s => s.page);
  names = merge(this.page, interval(10000).pipe(withLatestFrom(this.page, (_, page) => page)) )
      .pipe(
        switchMap(page => this.http.get(`https://api.github.com/orgs/ReactiveX/repos?page=${page}&per_page=5`)),
        map(res => res.map(i => i.name))
      );

  constructor(private store: Store<any>, private http: HttpClient) {  
  }

}


Enter fullscreen mode Exit fullscreen mode

Following things (roughly) happen here:

  • all values are retrieved by using the async pipe in the template
  • deriving the values from the page param from this.store by using the select method
  • deriving the HTTP result by combining the page observable with the HTTP observable
  • solving race conditions by using the switchMap operator
  • as all subscriptions are done by the async pipe we:
    • subscribe to all observables on AfterContentChecked
    • applying all arriving values to the pipes return value

On the next change detection run, we will see the latest emitted value in the template.

If the component gets destroyed Angular manages the subscription over the async pipe.

Avoiding Reactive Programming (🎮 demo)



import { Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs';

@Component({
  selector: 'example4-im',
  template: `
  <h2>Example4 - Avoid Reactive Programming</h2>
  Repositories Page [{{page}}]: 
  <ul>
    <li *ngFor="let name of names">{{name}}</li>
  </ul>
  `
})
export class Example4ImComponent implements OnDestroy {
  pageSub = new Subscription();
  page;

  intervalId;

  names;

  constructor(private store: Store<any>) {
    this.pageSub = this.store.select(s => s.page)
      .subscribe(page => {
        this.page = page;
        this.updateList()
    });

    this.intervalId = setInterval(() => {
        this.updateList();
    }, 10000)
  }

  updateList() {
    if(this.page === undefined) {
      return;
    }

    fetch(`https://api.github.com/orgs/ReactiveX/repos?page=${this.page}&per_page=5`)
      .then(result => result.json())
      .then((res: any) => this.names = res.map(i => i.name));
  }

  ngOnDestroy() {
    this.pageSub.unsubscribe();
    clearInterval(this.intervalId);
  }
}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • retrieving the new state by subscribing in the constructor
  • deriving the values from the page param and fetch data from this.store by using the select method and call subscribe
  • storing the returned subscription from the subscribe call under pageSub
  • in the store subscription we:
    • assign the arriving value to the components page property
    • we take the page value and create an HTTP get call by using the fetch API.
    • an HTTP get request fires
    • we retrieve the result in .then() call of the returned Promise
      • we convert the response to JSON format
      • we assign the value to a static class property

Here we have to manage the active processes in case the component gets destroyed.

  • when the component gets destroyed
    • we call this.pageSub.unsubscribe() in the ngOnDestroy life-cycle hook
    • we call clearInterval(intervalId); in the ngOnDestroy life-cycle hook

As I nearly always code reactive in Angular projects I never think about teardown logic.
Therefore I forgot that a tiny bit of logic made a critical mistake. blush

I forgot to dispose of the returned Promise from the fetch call, and therefore I didn't handle race conditions and we have a bug in our code.

So I also implemented a solution for the race condition of the HTTP calls.
To solve it I used another API, the AbortController's signal and abort functions.

I created a method on my component disposableFetch and a property httpAbortController.
The method takes the URL and a callback as a second parameter.
It fires the request, provides the signal from the created AbortController to the fetch call and passes the result to the callback function.
Then it returns the created AbortController to give others the option to dispose of the fetch call.

Avoiding Reactive Programming Fixed (🎮 demo)



import { Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs';

@Component({
  selector: 'example4-pr',
  template: `
  <h2>Example4 - Use Promises</h2>
  Repositories Page [{{page}}]: 
  <ul>
    <li *ngFor="let name of names">{{name}}</li>
  </ul>
  `
})
export class Example4ImFixedComponent implements OnDestroy {
  pageSub = new Subscription();
  page;

  intervalId;

  httpAbortController;
  names;

  constructor(private store: Store<any>) {
    this.pageSub = this.store.select(s => s.page)
      .subscribe(page => {
        this.page = page;
        this.updateList()
    });

    this.intervalId = setInterval(() => {
        this.updateList();
    }, 10000);
  }

  updateList() {
    if(this.page === undefined) {
      return;
    }

    if(this.httpAbortController) {
      this.httpAbortController.abort();
      this.httpAbortController = undefined;
    }

    this.httpAbortController = this.disposableFetch(
      `https://api.github.com/orgs/ReactiveX/repos?page=${this.page}&per_page=5`, 
      (res: any) => this.names = res.map(i => i.name));
  }

  ngOnDestroy() {
    this.pageSub.unsubscribe();
    clearInterval(this.intervalId);
    if(this.httpAbortController) {
      this.httpAbortController.abort();
      this.httpAbortController = undefined;
    }
  }

  disposableFetch(url, callback): AbortController {
    const httpController = new AbortController();
    const httpSignal = httpController.signal;

    fetch(url, {signal: httpSignal})
      .then(result => result.json())
      .then(callback);

    return httpController;
  }

}


Enter fullscreen mode Exit fullscreen mode

The following things happen here:

  • retrieving the new state by subscribing in the constructor
  • deriving the values from the page param from this.store by using the select method call subscribe
  • we store the returned subscription from the subscribe call under pageSub
  • in the store subscription we:
    • assign the arriving value to the components page property
    • we take the page value and create an HTTP get call over the new disposableFetch method`
    • if httpAbortController is a AbortController we call AbortController.abort()
      • we reset httpAbortController to undefined
    • we create a new AbortController and provide its signal to the fetch call
    • we use the fetch API again
      • an HTTP get request fires
      • we retrieve the result in .then() call of the returned Promise
        • we convert the response to JSON format
        • we assign the value to the static class property
    • we store the AbortController returned by disposableFetch at under a component property

Here we have to manage the active processes in case the component gets destroyed.

  • when the component gets destroyed
    • we call this.pageSub.unsubscribe() in the ngOnDestroy life-cycle hook
    • we call clearInterval(intervalId); in the ngOnDestroy life-cycle hook
    • we call httpAbortController.abort(); in the ngOnDestroy life-cycle hook

My 2 cents

I got told so many times that you have to learn RxJS to be able to use Angular.
Even if it was easy for me to avoid it, it doesn’t seem easy to other people... This made me create this writing.
It hopefully showed that it is very easy to avoid reactive programming in Angular (even if you use reactive third party libs).

Nevertheless, I want to share my personal opinion and experience with you.

RxJS gave me a hard time learning it, but the code I produced with it was (mostly ;P) more maintainable, stable and elegant than any other.
Especially in the front-end, it gives me a tool to model and compost complex asynchronous processes in a way that impresses me even today.

If we take the above examples we see that if we don't use observables we:

  • produce more lines of code
  • the level of complexity in the code is much higher
  • we have to put the logic for one process in multiple places
  • it is very hard to maintain the code
  • it is very hard to add features
  • even the number of indentations in the textual process description is way deeper

IMHO it is worth the headache, that you will for sure get if you try to learn RxJS
and even more worth the money that the company spends on your learning.

Btw, I do trainings and workshops ;)

Summary

We used different APIs for the imperative approach:

  • addEventListener and removeEventListener
  • new Promise and no dispose logic implemented
  • new AbortController and AbortController.abort
  • setInterval as clearInterval

instead of the reactive (functional reactive) approach:

  • subscribe and unsubscribe

And we maintained the state in a mutable instead of an immutable way.

How to avoiding reactive programming in angular:

  • Calling .subscribe() ends reactive programming. Do it as early as possible!
  • You can use helpers to hide away the subscription handling
  • Don't mix it!
  • Functional Reactive Programming is a lot of headaches and has steep learning curve
  • IMHO FRP pays off quickly if you need to compose asynchronous processes

Thanks to @niklas_wortmann for the really helpful feedback and review <3 <3 <3


Resources

You can find the source code of the examples

as well as all the resources in the repository How to Avoid Observables in Angular on GitHub.

The all in one solution to handle Observables and subscriptions:
📦 @rx-angular/state

For Ivy (Angular >= 9):

@MikeRyanDev

@EliranEliassy

For ViewEngine (Angular <= 8):

@NetanelBasal

Glossary

  • functional programming: Using functions and immutable state
  • reactive programming: A style of functional programming where we process incoming events as we would do with lists (JavaScript Arrays)
  • imperative programming: Using objects and mutable state
  • single-shot observable: As a Promise completes after the value it emitted, these observables emit a single value and then complete.
  • on-going observable: like an interval fires multiple values over time these are observables that need to get completed manually.
  • functional composition: Therm in functional programming that is a mechanism to combine simple functions to build more complicated ones.
  • closure: Therm in functional programming that is a function storing a value together with another function scope.
  • HOC: Acronym for Higher Order Component
  • inheritance: Is the mechanism of basing a class instance upon another one, to retaining similar implementation
  • class-level decorators: TypeScript Documantation
  • broken UI state: An inconsistency in rendered state and stored state
  • race-condition: Problem of asynchronous programming where two or more operations are accidentally done in the wrong sequence.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .