Observables for large-scale Angular apps
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
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
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
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.