Recently, I've noticed several projects that treat REST calls like streams by using RxJS. However, this approach can lead to convoluted and difficult-to-follow code. In this article, I will explain the differences between these two approaches and highlight the pitfalls of handling HttpClient REST calls as streams.

What's the difference?

In RxJS, a stream is a sequence of data that can be observed over time, with each piece of data being emitted (sent) individually. This is different from an HttpClient REST call, which is a single request-response cycle between a client and server that returns a complete response. In other words, a stream allows for a continuous flow of data, while a REST call is a one-time request. To clarify, when making HttpClient REST calls in RxJS, there is no need to use an AsyncPipe or unsubscribe from it.

An Example

A simple user data table, with a refresh button, loading and error state.

Handle it like a stream

1// users.component.html
2<button (click)="refresh()" [disabled]="loading">
3  Refresh
4</button>
5<p *ngIf="loading">
6 Loading…
7</p>
8<p *ngIf="error">
9 Something went wrong…
10</p>
11<table *ngIf="users$ | async as users">
12  <thead>
13    <tr>
14      <th>ID</th>
15      <th>Name</th>
16      <th>Email</th>
17    </tr>
18  </thead>
19  <tbody>
20    <tr *ngFor="let user of users">
21      <td>{{ user.id }}</td>
22      <td>{{ user.name }}</td>
23      <td>{{ user.email }}</td>
24    </tr>
25  </tbody>
26</table>
27
28// users.component.ts
29import { Component } from '@angular/core';
30import { UserService } from './user.service';
31import { User } from './user.interface';
32import { Observable } from 'rxjs';
33import { tap, catchError, switchMap, map } from 'rxjs/operators';
34
35@Component({
36  selector: 'app-users',
37  templateUrl: './users.component.html',
38  styleUrls: ['./users.component.css']
39})
40export class UsersComponent {
41  public users$: Observable<User[]> = this.userService.getUsers().pipe(
42    switchMap(response => this.refreshUsers$.pipe(
43      map(() => response))
44    ),
45    catchError(error => {
46      console.info(error);
47      this.error = true;
48    }),
49    tap(() => this.loading = false)
50  );
51  private refreshUsersSubject = new BehaviorSubject(null);
52  private refreshUsers$ = this.refreshUsersSubject.asObservable();  
53  public loading = true;
54  public error = false;
55
56  constructor(private userService: UserService) {}
57
58  public refresh() {
59    this.loading = true;
60    this.error = false;
61    this.refreshUsersSubject.next(null);
62  }
63
64}
1// users.component.html
2<button (click)="refresh()" [disabled]="loading">
3  Refresh
4</button>
5<p *ngIf="loading">
6 Loading…
7</p>
8<p *ngIf="error">
9 Something went wrong…
10</p>
11<table *ngIf="users$ | async as users">
12  <thead>
13    <tr>
14      <th>ID</th>
15      <th>Name</th>
16      <th>Email</th>
17    </tr>
18  </thead>
19  <tbody>
20    <tr *ngFor="let user of users">
21      <td>{{ user.id }}</td>
22      <td>{{ user.name }}</td>
23      <td>{{ user.email }}</td>
24    </tr>
25  </tbody>
26</table>
27
28// users.component.ts
29import { Component } from '@angular/core';
30import { UserService } from './user.service';
31import { User } from './user.interface';
32import { Observable } from 'rxjs';
33import { tap, catchError, switchMap, map } from 'rxjs/operators';
34
35@Component({
36  selector: 'app-users',
37  templateUrl: './users.component.html',
38  styleUrls: ['./users.component.css']
39})
40export class UsersComponent {
41  public users$: Observable<User[]> = this.userService.getUsers().pipe(
42    switchMap(response => this.refreshUsers$.pipe(
43      map(() => response))
44    ),
45    catchError(error => {
46      console.info(error);
47      this.error = true;
48    }),
49    tap(() => this.loading = false)
50  );
51  private refreshUsersSubject = new BehaviorSubject(null);
52  private refreshUsers$ = this.refreshUsersSubject.asObservable();  
53  public loading = true;
54  public error = false;
55
56  constructor(private userService: UserService) {}
57
58  public refresh() {
59    this.loading = true;
60    this.error = false;
61    this.refreshUsersSubject.next(null);
62  }
63
64}

This example relies heavily on RxJS operators, which can be complex and difficult to manage. As a best practice, it is recommended to keep RxJS manipulation operators out of components and instead use Angular services when they are really needed. This will help to keep your components clean and more easily maintainable. It's important to note that the code provided here is a simple example, and even in this case we're already encountering issues with the refresh mechanism. Therefore, it's important to approach the use of RxJS with caution and use it judiciously in order to avoid convoluted and hard-to-manage code.

Handle it like a single request-response

1// users.component.html
2<button (click)="setUsers()" [disabled]="loading">
3  Refresh
4</button>
5<p *ngIf="loading">
6 Loading…
7</p>
8<p *ngIf="error">
9 Something went wrong…
10</p>
11<table *ngIf="!loading && !error">
12  <thead>
13    <tr>
14      <th>ID</th>
15      <th>Name</th>
16      <th>Email</th>
17    </tr>
18  </thead>
19  <tbody>
20    <tr *ngFor="let user of users">
21      <td>{{ user.id }}</td>
22      <td>{{ user.name }}</td>
23      <td>{{ user.email }}</td>
24    </tr>
25  </tbody>
26</table>
27
28// users.component.ts
29import { Component, OnInit } from '@angular/core';
30import { UserService } from './user.service';
31import { User } from './user.interface';
32
33@Component({
34  selector: 'app-users',
35  templateUrl: './users.component.html',
36  styleUrls: ['./users.component.css']
37})
38export class UsersComponent implements OnInit {
39  public users: User[] = [];
40  public loading = false;
41  public error = false;
42
43  constructor(private userService: UserService) {}
44
45  public setUsers() {
46    this.loading = true;
47    this.error = false;
48    this.userService.getUsers().subscribe({
49      next: response => {
50        this.users = response;
51        this.loading = false;
52      },
53      error: error => {
54        console.error(error);
55        this.loading = false;
56        this.error = true;
57      }
58    });
59  }
60
61  ngOnInit() {
62    this.setUsers();
63  }
64
65}
1// users.component.html
2<button (click)="setUsers()" [disabled]="loading">
3  Refresh
4</button>
5<p *ngIf="loading">
6 Loading…
7</p>
8<p *ngIf="error">
9 Something went wrong…
10</p>
11<table *ngIf="!loading && !error">
12  <thead>
13    <tr>
14      <th>ID</th>
15      <th>Name</th>
16      <th>Email</th>
17    </tr>
18  </thead>
19  <tbody>
20    <tr *ngFor="let user of users">
21      <td>{{ user.id }}</td>
22      <td>{{ user.name }}</td>
23      <td>{{ user.email }}</td>
24    </tr>
25  </tbody>
26</table>
27
28// users.component.ts
29import { Component, OnInit } from '@angular/core';
30import { UserService } from './user.service';
31import { User } from './user.interface';
32
33@Component({
34  selector: 'app-users',
35  templateUrl: './users.component.html',
36  styleUrls: ['./users.component.css']
37})
38export class UsersComponent implements OnInit {
39  public users: User[] = [];
40  public loading = false;
41  public error = false;
42
43  constructor(private userService: UserService) {}
44
45  public setUsers() {
46    this.loading = true;
47    this.error = false;
48    this.userService.getUsers().subscribe({
49      next: response => {
50        this.users = response;
51        this.loading = false;
52      },
53      error: error => {
54        console.error(error);
55        this.loading = false;
56        this.error = true;
57      }
58    });
59  }
60
61  ngOnInit() {
62    this.setUsers();
63  }
64
65}

As you can see in this code, we removed the unnecessary use of the AsyncPipe and use setUsers() method that retrieves/refreshes the users from the service and updates the component's state accordingly. This approach allowed us to remove the need for several RxJS operators and simplify the logic, resulting in code that is now much easier to understand and maintain. Additionally, we now have fewer imports, which not only reduces the overall complexity of the file but also helps improve the application's performance by reducing the size of the bundle that needs to be loaded.

Conclusion

While RxJS can be a powerful tool, it's important to use it judiciously and not rely on complex operators in your components. Instead, keep heavy data manipulation in services and use simple, straightforward methods in your components. I can understand why developers want to follow one pattern to coding but, you should always make sure that your architecture and patterns make sense for your specific use case, both now and in the future. In other words, keep your code KISS - Keep It Simple, Stupid.

Update

I was curious about how much slower the reactive variant would be, so I added some benchmarks using console.time()/console.timeEnd() for both the initial data fetch and the refresh, and ran it five times:

First Fetch

Plain SubscribeReactive
1.833740234375 ms2.68505859375 ms
2.861083984375 ms4.126220703125 ms
4.0830078125 ms5.638671875 ms
2.82177734375 ms3.94482421875 ms
3.593994140625 ms5.013916015625 ms

Refresh

Plain SubscribeReactive
0.0341796875 ms5.075927734375 ms
0.046875 ms5.416748046875 ms
0.041015625 ms5.6728515625 ms
0.0498046875 ms7.52099609375 ms
0.048095703125 ms5.950927734375 ms

Imagine now using a bunch of RxJS operators in the component, just to follow the reactive pattern. You will soon run into performance issues.