In the first post of this series, we created a synchronous type ahead. Now this time we are going to fetch data via an API. Here for the first time, we will see some great use cases of Rxjs like cancellable requests, race condition handling, etc. If you have not read the first article you can read it here.
In this article, we will use some really powerful RxJs operators like switchMap, catchError, filter.
After creating the synchronous data type ahead first we should think about thechanges that we should make.
We need to replace the getContinents function with a function that makes an API call and return us an observable so that we can plug it into our observable stream.
Handle race conditions (while typing in a type ahead a new request may resolve before the first request which may lead to a mismatch of data).
For our first step, I found an open API for countries on https://restcountries.eu/. Here the API endpoint
https://restcountries.eu/rest/v2/name/{name}
to integrate this we have to make an API call to this endpoint. For that, I had to search for a few a minutes after searching I found ajax operator (It creates an observable for an Ajax request with either a request object with URL, headers, etc or a string for a URL) which creates ajax requests. After using this operator I found out that it returns a Response object which we have to cover again but after reading through docs I found out to get the data we can use ajax.getJSON() to fetch data from API. Yay!
const countriesRequest = keys => ajax.getJSON(`https://restcountries.eu/rest/v2/name/${keys}`)
this returns an observable with the data of countries.
We have the data now we have to think about how to integrate it into our stream. We have to map out keys we press to the observable of data to do that we can use the map operator
fromEvent(document.getElementById("type-ahead"), "keyup")
.pipe(
debounceTime(200),
map((e: any) => e.target.value),
distinctUntilChanged(),
map(countriesRequest)
).subscribe(console.log);
but now we have a new inner observable, I think it'll be easy to work with it if we merge this inner observable with the outer observable stream. For that I searched "change data from outer Observable to inner Observable" I got some results such as switchMap, flatMap, mergeMap, exhaustMap. After reading through all the definitions we I decided to use the switchMap operator because it solves our race condition problem too, Yay! So let us understand what the switchMap operator does( On each emission, of the inner observable the previous inner observable the result of the function you supplied is canceled and the new observable is subscribed). So switchMap essentially maps the outer observable to the inner observable and cancels any previous requests which are not completed. Which solves our race condition problem.
switchMap(countriesRequest)
This nearly resolves all our problems and we are nearly done. Now we just need to convert the data into the required form and render the results. This can be done via map and tap now we need to convert the array of objects to an array of names and we render again with the tap operator exactly in the last exercise.
map(resp => resp.map(country => country.name)),
tap(c => (document.getElementById("output").innerText = c.join("\n")))
Combining all the results
const countriesRequest = keys =>
ajax.getJSON<Country[]>(https://restcountries.eu/rest/v2/name/${keys});
fromEvent(document.getElementById("type-ahead"), "keyup")
.pipe(
debounceTime(200),
map((e: any) => e.target.value),
distinctUntilChanged(),
switchMap(countriesRequest),
map(resp => resp.map(country => country.name)),
tap(c => (document.getElementById("output").innerText = c.join("\n")))
)
.subscribe();
It seems like we are nearly there but while I was testing I found out that this API gives a 404 error when there are no results and this breaks our stream and no further event processing is done. After searching on the internet I found out this is the way RxJs works if our inner observable throws an error our observable stream breaks. To solve this first I thought I should filter out all the events in which we have the input value as an empty string. For filtering, we will use the filter operator
fromEvent(document.getElementById("type-ahead"), "keyup")
.pipe(
debounceTime(200),
map((e: any) => e.target.value),
filter(e => !!e),
distinctUntilChanged(),
switchMap(countriesRequest),
map(resp => resp.map(country => country.name)),
tap(c => (document.getElementById("output").innerText = c.join("\n")))
)
.subscribe();
but still, the problem persists when we put random digits or something like cjkdshckjsdh . Which again breaks our stream. To resolve this we have to maintain the stream even if an error occurs. This felt same as handling errors. So, I searched for error handling in RxJs and found out about the catchError operator this lets us catch an error and return a new observable whenever the error occurs. I thought we should return an object with name property as No countries found. This will keep our stream alive and will give a nice message whenever we receive no results from the API. So, now let us look at the final results.
const countriesRequest = keys =>
ajax.getJSON(`https://restcountries.eu/rest/v2/name/${keys}`)
.pipe(catchError(() => of([{ name: "No countries found" }])));
fromEvent(document.getElementById("type-ahead"), "keyup")
.pipe(
debounceTime(200),
map((e: any) => e.target.value),
filter(e => !!e),
tap(console.log),
distinctUntilChanged(),
switchMap(countriesRequest),
map(resp => resp.map(country => country.name)),
tap(c => (document.getElementById("output").innerText = c.join("\n")))
)
.subscribe();
You can see the final result here.
We can see that RxJs has many powerful operators and it can create many powerful data streams. Try creating a Promise based typeahead and you will surely realize how easy RxJs makes our life.
If you have any comments or advice please write it down in the comments below.
If you like my work please support me at https://www.buymeacoffee.com/ajitsinghkaler
Top comments (0)