Loaders is an integral part of user experience. Appropriate usage of loaders is essential to ensure smooth experience. If a loader stops too early, it feels like nothing has happened and seems like the app has frozen. If the loader stops too late, then it feels like an eternity to use the app. In both the cases, you loose a user and in extreme cases, are liable for some cuss-words too!
Now if you are working in Angular, then you are obviously working with Observables. But if you are working with React, and are using RxJS Observables to control the data flow (the reactive paradigm), even then you can use the following technique to ensure perfect start-stop of loaders.
Observable lifecycles
There are 3 stages in the lifecycle:
next
– This is when the observable completes with a success and sends data to the subscribererror
– When the observable’s execution throws an error and sends an error object to the subscribercomplete
– When the execution is completed but no data is sent to the subscriber
Say, you start the loader before making the API call. The tricky part is when and how to stop the loader using the Observable lifecycle.
STOPPING AT EVERY STEP
// initially the loader is false
this.isLoading = false;
// fetch todos
fetchTodos() {
// start the loader
this.isLoading = true;
const http$ = this.http.get('https://jsonplaceholder.typicode.com/todos');
http$.subscribe(
next => {
console.log('Data from API', next);
// stop the loader once the observable completes with success
this.isLoading = false;
},
error => {
console.log('Error from API', error);
// stop the loader if the observable fails
this.isLoading = false;
}
);
}
Play with the demo here: Stackblitz Link
Using complete
The loader in this case will stop only when the observable completes with a success. If the observable fails with an error, we’ll have to still explicitly stop the loader in the error block.
// initially the loader is false
this.isLoading = false;
fetchTodos() {
this.isLoading = true;
const http$ = this.http.get('https://jsonplaceholder.typicode.com/todos');
http$.subscribe(
next => {
console.log('Data from API', next);
},
error => {
console.log('Error from API', error);
// stop the loader if the observable fails
this.isLoading = false;
},
() => {
// onComplete block
// stop the loader once the observable completes with success
this.isLoading = false;
}
);
}
Play with the demo here: Stackblitz Link
BEST WAY: RxJS finalize
operator
This will help you to stop the loader in both cases, when the observable execution completes in success or when it fails.
For this, you’ll first have to import the finalize operator from RxJS.
import { finalize } from 'rxjs/operators';
Once done, you can use this operator with the pipe operator, just before you subscribe.
// initially the loader is false
this.isLoading = false;
fetchTodos() {
this.isLoading = true;
const http$ = this.http.get('https://jsonplaceholder.typicode.com/todos');
http$
.pipe(
finalize(() => {
this.isLoading = false;
})
)
.subscribe(
next => {
console.log('Data from API', next);
},
error => {
console.log('Error from API', error);
}
);
}
Play with the demo here: Stackblitz Link
Here, you don’t have to stop the loader explicitly within “next” and “error” blocks. The loader will be stopped in the “finalize” block in both the cases:
- When the observable completes to success
- When the observable completes to error
Faking a Failed HTTP Request
To check the stopping of loading in case the observable throws an error, we can fake a failed API response by throwing an error on purpose. For this, we’ll be using RxJS operators like map.
this.isLoading = false;
fetchTodos() {
this.isLoading = true;
const http$ = this.http.get('https://jsonplaceholder.typicode.com/todos');
http$
.pipe(
map(d => {
// deliberately throwing an error
throw new Error('test error');
})
)
.subscribe(
next => {
console.log('Data from API', next);
this.data = next;
this.isLoading = false;
},
error => {
this.data = [];
console.error('Error from API', error);
this.isLoading = false;
}
);
}
Play with the demo here: Stackblitz Link
The “map” operator is generally used to modify the incoming data before we can use it in the subscribe block. Here, we are using the map-block to throw an Error and hence the error block will get executed.
Hidden Gem – .add()
If you feel like “finalize” doesn’t makes sense according to flow because we are writing code to stop the loader before everything, there is .add() operator for you. It behaves same like the finalize operator and gets executed in both the cases – success or error.
let isLoading = false;
fetchTodos() {
this.isLoading = true;
const http$ = this.http.get('https://jsonplaceholder.typicode.com/todos');
http$
.subscribe(
next => {
console.log('Data from API', next);
this.data = next;
},
error => {
this.data = [];
console.error('Error from API', error);
}
).add(() => {
this.isLoading = false;
});
}
Play with the demo here: Stackblitz Link
In Conclusion…
The one thing which is not addressed here is unsubscribing of Observables but I’ll surely cover that in the upcoming post. This was just to bring to your attention that Observables are sneaky.
I learnt this after a number of trial and errors and its such a small thing which is being used in every project I do. There is one other way to start-stop the loader globally in the app which uses RxJS BehaviorSubject. I’ll try to update the post with this in future as well!
Until then, share this with your friends/colleagues. Any suggestions/ideas/advice/feedback – please reach out to me:
- In the comments below
- Email - tyagi.aditya844747@gmail.com
- Twitter - @secondbestcoder
Originally poster on adityatyagi.com