When I started using Angular (not AngularJS) there where 2 things I wasn’t overly thrilled about:
At least with Typescript, I could see the benefits in debugging and refactoring. But Observables? If I have to write a function to handle a XHR request, I may as well just use a Promise
.
What changed my mind
What got me onboard was how easily you can handle race conditions on XHR requests. Let’s say you have an input field that, as the user inputs a value, you perform a search.
To avoid sending unnecessary requests, you add a debounce
to the input event listener. But that change only reduces the amount of requests, it doesn’t handle any race conditions with the responses. You can still have the situation where:
- the user inputs something
- pauses
- then the user updates the input value
- And for whatever reason, the second request is returned before the first
Maybe:
- the database is slow for the first request,
- or the data returned for request 1 is so much larger than request 2 so it takes longer to receive.
Even if the request, comes back in the correct order, the resolve
function will run twice, leading to flickering of content and unnecessary function calls.
To solve this using RXJS:
fromEvent(fieldEl, 'change')
.pipe(
switchMap((term) =>fromPromise($.get(https://exampleapi.com/${term}
)))
).subscribe( (result) => console.log(result));
fromEvent
creates an observable from change property on the field and fromPromise
creates an observable from a promise.
Observables add the ability to ‘cancel’ the response handler. No matter how many times the user changes the input, it is only the last request that is logged to the console.
However we are still making a request every time the user updates the input field. Let’s add a 200 millisecond debounce to the input:
fromEvent(fieldEl, 'change')
.pipe(
debounce(200),
switchMap((term) =>fromPromise($.get(https://exampleapi.com/${term}
)))
).subscribe( (result) => console.log(result));
Now, let’s only send the request if it has changed (in case the user accidentally deletes a character and adds it back)
fromEvent(fieldEl, 'change')
.pipe(
debounce(200),
distinctUntilChanged(),
switchMap((term) =>
fromPromise($.get(`https://exampleapi.com/${term}`)))
).subscribe( (result) => console.log(result));
For another example, lets say you want to run a function only the first time the user clicks on a button
fromEvent(btn, 'click')
.pipe(take(1))
.subscribe((result)=>console.log(result))
Conversely, if you want to run the function every but the first:
fromEvent(btn, 'click')
.pipe(skip(1))
.subscribe((result)=>console.log(result))
if you combine them, the function will only be called on the second click:
fromEvent(btn, 'click')
.pipe(skip(1),take(1))
.subscribe((result)=>console.log(result))
And finally, you want it to be 2 distinct clicks, not a double click
fromEvent(btn, 'click')
.pipe(debounce(200),skip(1),take(1))
.subscribe((result)=>console.log(result))
It was this simplicity that made me want to learn more about RxJS. However, the real reason I continue to use it is because it made me re-evaluate how I think about and model my data in an application. I’ll write more about this in a later post.
In part 2 of this series, we will talk about how RXJS is bigger than network requests how we can combine observables to check in on Schrödinger’s cat.