Mastering RxJS: operators and functions that can bite you when you don’t expect
The things you may not pay attention to but they are good to know
Prerequisites: You should be familiar with RxJS and have some practical experience in using it.
RxJS lib has two ways to wrap network request in Observable. And They have difference:
fromFetch function — uses the Fetch API to make an HTTP request.
2. ajax function — use XhrHttpRequest under the hood
Both return Observable so we expect unfinished network request should be canceled on unsubscribe. Usually it works like that but there is small remark in the official documentation for fromFetch:
WARNING Parts of the fetch API are still experimental.
AbortControlleris required for this implementation to work and use cancellation appropriately.
Will automatically set up an internal AbortController in order to teardown the internal
fetchwhen the subscription tears down.
So beware if you use it in IE11 browser since AbortController is not supported there.
So how it works:
- We create many Observables that will do HTTP requests with fromFetch function.
- zip function gets an array of such Observables and subscribes them causing HTTP requests to be performed.
3. Zip waits until every argument Observable emit values with same index (index = 1 (or 0:) in our case) and emits an array of values .
4. Since all argument Observables produce only one value — so after all responses are fetched — zip produce array of responses.
You may think — everything is OK. And actually it is:) But only in this particular case. Why? because If you try to feed zip function with Observables that produce more then one value — you will get unexpected behavior (more then one emission or never-completed result Observable).
To prevent such possible drawbacks you can use forkJoin function.
forkJoin waits for all argument Observables to complete and then emit an array of last emitted values (to compare: zip emits an array of values with same emission index).
Now our example will look like this:
Now you are armed💪.
#3. Using materialize, dematerialize to mock delayed erred Observable
Say you have such function that makes a network request in Angular:
Now to mock HttpClient successful response and emulate network latency you can provide such value:
And you may think that to mock delayed erred response it is enough to do like this:
But it will not work the way you expect. Once throwError produce error value — delay will be ignored and your error handler will be run immediately.
How to fix that? How to prevent error event to omit delay operator?
Now our mock will look like this:
#4 Using timer function with one argument instead of of(0).pipe(delay(x))
Previously If I had a need to emit once with initial delay i used such expression:
It works well, but can we do it even better? Yes, with RxJS timer function. Timer definition looks like this:
timer(dueTime: number, period, scheduler)
duetime — initial delay before starting emissions
period — The period of time between emissions of the subsequent numbers.
But what if I need to emit only once? Just use timer with one dueTime argument:
Do you like the article? You can find more interesting RxJS staff in my video-course “Hands-on RxJS”. It may be interesting as for beginners (Section 1–3) as well as experiences RxJS developers (Sections 4–7). Buy it, watch it and leave comments and evaluations!
#5 takeLast with no param returns undefined
Once upon a time, I stepped on a rake by using takeLast() operator (RxJS ver 6.5.x). Just to remind you:
Waits until source is complete and then emits only the last
countvalues emitted by the source Observable.
Somehow I expected that if applied without any param it returns 1 last value by default:
But no — it returns undefined. To return 1 last value you should use it like this: takeLast(1):
You can check this behavior in this codepen. Beware!
#6 from(fetch(url)) — eager vs defer(()=>from(fetch(url)) — lazy.
OK, now let's go through it with explanations.
You know that Observables are lazy(do nothing until subscribed), Promises are eager (do everything when created) (more here).
Also, fetch returns Promise. This means that if we call fetch(‘url’) — network call will be performed at once. We can convert Promise to Observable with RxJS from the function but since fetch is called first — then anyway it will work in an eager way.
from(fetch(‘url’)) // will work in eager way
So how to fix it? We can use RxJS defer function.
defer(() => fetch(url)) //will work in lazy way
One more note — in RxJS we already have special function that implements lazy fetch method — fromFetch.
defer(() => fetch(url)) //will work in lazy way
fromFetch(url) //do the same
If you take a look at fromFetch sources — you may observable how it implements deferred behavior.
Small hint 😎:
Remember? We reviewed fromFetch in tip #1.
Typical switchMap operator example with the condition may look like:
It works this way:
- interval function generates incrementing numbers with some delay.
- switchMap callback checks if the emitted number is odd, if yes — it makes request wrapped in Observable and subscriber gets a result. If no — we return empty sequence constant — EMPTY (it completes at once).
Nothing special here. Recently I found out that we can us of() with no params to reach the same result. Now our example will look like:
You can check yourself and play with it here.
#8 toPromise gives you the last value when Observable completes. Be careful when you use it with Subjects.
Angular2 subreddit sometimes brings interesting knowledge as well :-)
I will comment the code from this help request:
- You have WarehouseService, that provides some value with RxJS BehaviorSubject. As usual, we have some method getDefaultWarehouse there to get this Subject as Observable:
So when DB is ready we emit row.defaultWarehouse value (see previous snippet) and in index.ts we await to get defWarehouse value.
But it never resolves. WHAT?
The reason is simple: RxJs method ‘toPromise()’ waits for your observable to complete. Since we use BehaviorSubject, that just emit value but don’t complete — toPromise will never be called.
You can solve it in two ways:
- If WarehouseService should emit value with BevahiorSubject only once — then just complete it after emission:
2. Or just subscribe to Observable to run a subsequent action.
You can play with the code here.
More to read:
#9 Different ways RxJS Subjects works after completion (Behavior, Replay, Async)
Recent Angular-in-Depth 2019 conference in Kyiv, Ukraine remind me about different behavior of RxJS BehaviorSubject, ReplaySubject and AsyncSubject after completion. Not to be very verbose — I just created a comparison table:
You can play with it here.
More to read and watch:
- Read Wojciech Trawiński’s article “BehaviorSubject vs ReplaySubject(1) -beware of edge cases”.
- Nice talk of Michael Hladky: “A deep dive into RxJS subjects” at AiD conference.
- “Understanding RxJS BehaviorSubject, ReplaySubject and AsyncSubject”.
- All Angular-in-Depth conference 2019 talks videos.
#10 If we reassign Observable used in Angular template with asyncPipe — it will continue working.
Usually, Angular asyncPipe usage looks something similar to this example:
And we know that asyncPipe will handle all the Observable subscribe/unsubscribe activity to prevent memory leaks.
But what if we just re-assign this.name Observable with another Observable instance? Will AsyncPipe do unsubscription? Will it re-subscribe to a new Observable instance? Let's check it out!
- I will add a button to re-assign this.name with new Observable instance.
And added console.log to unsubscribe method to observe whether previous observable is unsubscribed.
Now, let's check what happens when Observable property is re-assigned.
AS you can see — previous Observable is unsubscribed. Phew..we can sleep peacefully now!
You can check results in this Stackblitz playground.
So, seems like you’ve become 10 gotcha’s richer 💰.
Have your own tricky RxJS cases solved? Share in comments!