Observables for large-scale Angular apps

Sunday 15th of December 5:09 PM
Vicentiu B.

I've always been interested in best practices and patterns when it comes down to writing code.
Recently, while working with Angular and RxJS's Observables to build a large-scale web app, I discovered a few improvements to the "default" example.

Let's take a look at the HttpClient example from angular.io

app/config/config.component.ts

showConfig() {  
  this.configService.getConfig()  
    .subscribe((data: Config) => this.config = {  
        heroesUrl: data['heroesUrl'],  
        textfile:  data['textfile']  
    });  
}

app/config/config.service.ts

configUrl = 'assets/config.json';

getConfig() {
  return this.http.get(this.configUrl);
}

When including the http.get call and the subscribe in the component, logic starts to build up inside the component.
Imagine this when trying to do a few subsequent calls, error handling, or some data processing with the powerful RxJS (pipe, map, etc.).
The code can get big, and it's kind of a lot for a single component. What do you do when you need the same processing in another component?

One option would be to move the code (subscribe, error handling, processing) to the service. Below is an example I built on StackBlitz.
 

Attempt 1 - Subsequent calls using Subject

Try it live on StackBlitz

app/app.component.ts

user: any = {};
posts: any[] = [];
error: any;
subs: Subscription = new Subscription();

ngOnInit() {
  this.repository.getFirstUsersPosts();

  this.subs.add(this.repository.user$.subscribe(user => this.user = user));
  this.subs.add(this.repository.posts$.subscribe(posts => this.posts = posts));
  this.subs.add(this.repository.error$.subscribe(error => this.error = error));
}

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

app/repository/repository.service.ts

private usersUrl = 'https://jsonplaceholder.typicode.com/users';
private postsUrl = 'https://jsonplaceholder.typicode.com/posts?userId=';
public user$ = new Subject<any>();
public posts$ = new Subject<any>();
public error$ = new Subject<any>();

getFirstUsersPosts() {
  this.http.get(this.usersUrl)
    .subscribe(result => this.usersResult(result), error => this.handleFailure(error));
}

private usersResult(users) {
  this.http.get(this.postsUrl + users[0].id)
  .subscribe(posts => this.postsResult(users[0], posts), error => this.handleFailure(error));
}

private postsResult(user, posts){
  this.user$.next(user)
  this.posts$.next(posts);
}

private handleFailure(error) {
  this.error$.next(error);
}

If you've already seen some issues here, I don't blame you. I don't like it that much either.
Problems:

  • By moving the subscribe to the service, we lose hold on the data, so we need another Subject/Subscription to retrieve the data.
  • The subscriptions need to be unsubscribed
  • It's a lot of code
      

Attempt 2 - Subsequent calls using callbacks

Try it live on StackBlitz

That's right, the good ol' callbacks...

app/app.component.ts

user: any = {};
posts: any[] = [];
error: any;

ngOnInit() {
  this.repository.getFirstUsersPosts(([user, posts], error) => {
    this.user = user;
    this.posts = posts;
    this.error = error;
  });
}

app/repository/repository.service.ts

private usersUrl = 'https://jsonplaceholder.typicode.com/users';
private postsUrl = 'https://jsonplaceholder.typicode.com/posts?userId=';

getFirstUsersPosts(callback) {
  this.http.get(this.usersUrl)
    .subscribe(users => this.usersResult(users, callback), error => callback(null, error));
}

private usersResult(users, callback) {
  this.http.get(this.postsUrl + users[0].id)
    .subscribe(posts => this.postsResult(users[0], posts, callback), error => callback(null, error));
}

private postsResult(user, posts, callback) {
  callback([user, posts]);
}

Now, the code is a bit cleaner and you can already see that you have less Subscriptions to deal with.
Error handling can also be handled in the same function.

But where this approach really shines, is when I want to change the wiring in the service and everything stays the same in the component.
Below is an example where we're going to call two endpoints simultaneously, but the component does not need to know that.
 

Attempt 3 - Simultaneous calls using callbacks

Try it live on StackBlitz

app/app.component.ts

user: any = {};
post: any = {};
error: any;

ngOnInit() {
  this.repository.getUsersAndPosts(([users, posts], error) => {
    this.user = users[0];
    this.post = posts[0];
    this.error = error;
  });
}

app/repository/repository.service.ts

private usersUrl = 'https://jsonplaceholder.typicode.com/users';
private postsUrl = 'https://jsonplaceholder.typicode.com/posts';

getUsersAndPosts(callback) {
  forkJoin(
    this.http.get(this.usersUrl),
    this.http.get(this.postsUrl)
  ).subscribe(([users, posts]) => callback([users,posts]), error => callback(null, error));
}

Now, the code is simple, and we can always add more processing to the service, without changing the component.
This also enables us to easily call the same method from other components, without duplicating the code.

You might say that you're too good for callbacks, I thought that too, but I tend to like the code in the second part better than the one in the first part.

Vicentiu B.  

Passionate full-stack developer with an eye for User Interface and flashy new web features