If you are confused about the differences between forkJoin
, zip
, combineLatest
and withLatestFrom
, you are not alone! 🙂
These 4 operators are what we know as
combination operators
– we use them when we need to join information from multiple observables.
Which operator should I use?
That is what this article is for! We will talk about the usage and differences between these 4 operators in an easy to understand way, so you know which one to choose when the time comes.
Setup
Imagine you are printing t-shirts. Ms. Color holds the color
information and Mr. Logo holds the logo
information. Both of them will pick color
and logo
spontaneously. You will need to wait and combine these two information continuously in order to print t-shirts. Ms. Color and Mr. Logo represent two observables in our code – color$
and logo$
.
// 0. Import Rxjs operators
import { forkJoin, zip, combineLatest, Subject } from 'rxjs';
import { withLatestFrom, take, first } from 'rxjs/operators';
// 1. Define shirt color and logo options
type Color = 'white' | 'green' | 'red' | 'blue';
type Logo = 'fish' | 'dog' | 'bird' | 'cow';
// 2. Create the two persons - color and logo observables,
// They will communicate with us later (when we subscribe)
const color$ = new Subject<Color>();
const logo$ = new Subject<Logo>();
// 3. We are ready to start printing shirt. Need to subscribe to color and logo observables to produce shirts, we will write code here later
...
// 4. The two persons(observables) are doing their job, picking color and logo
color$.next('white');
logo$.next('fish');
color$.next('green');
logo$.next('dog');
color$.next('red');
logo$.next('bird');
color$.next('blue');
// 5. When the two persons(observables) has no more info, they said bye bye.. We will write code here later
...
I guess the code above is pretty expressive itself. We created two observables by using Subject. For part 4 in the code, every .next(<value>)
means Ms. Color or Mr. Logo is picking color or logo.
Take note of the sequence of information (part 4 in our code), here is the summary:-
1. Ms. Color picks WHITE
2. Mr. Logo picks FISH
3. Ms. Color picks GREEN
4. Mr. Logo picks DOG
5. Ms. Color picks RED
6. Mr. Logo picks BIRD
7. Ms. Color picks BLUE
Later, we will update our code (part 3 & 5) to subscribe to both color and logo observables using the 4 different operators to see how the shirts are produced differently.
All set. Let’s start exploring our first operator!
zip – the love birds operator
I call zip
operator the love birds operator. Love birds need to always be together. Remember Titanic, the “you jump, I jump” type.
Let’s replace our code (part 3) with below:
// 3. We are ready to start printing shirt...
zip(color$, logo$)
.subscribe(([color, logo]) => console.log(`${color} shirt with ${logo}`));
TL;DR
For those of you who are not familar with JavaScript ES6/ES2015 destructuring assignment, you might find the syntax in subscribe [color, logo]
a little bit odd.
When we zip color$
and logo$
, we expect to receive an array of 2 items during subscribe
, first item is color
and second is logo
(follow their orders in zip
function).
The traditional way of writing it would be .subscribe((data) => console.log(
${data[0]} shirt with ${data[1]}))
. As you can see, it’s not very obvious that data[0]
is color.
ES6 allows us to unpack the value from arrays. Therefore, we unpack data
into [color, logo]
straight away. More readable right?
Result
Alright, let’s go back to our code and run it. The shirt printing result would be:-
Here is what get to log in the console:
1. white shirt with fish
2. green shirt with dog
3. red shirt with bird
How does zip
work?
Again, zip
operator is the love birds operator. In our case, color
will wait for logo
whenever there are new value (vice versa). Both values must change then only the log gets triggered.
1. Ms. Color picks WHITE
2. Mr. Logo picks FISH <- log 01, WHITE + FISH in pair, love birds!
3. Ms. Color picks GREEN
4. Mr. Logo picks DOG <- log 02, GREEN + DOG in pair, love birds!
5. Ms. Color picks RED
6. Mr. Logo picks BIRD <- log 03, RED + BIRD in pair love birds!
7. Ms. Color picks BLUE <- waiting for love...
zip
operator can accept more than 2 observables – no matter how many observables, they must all wait for each other, no man left behind!
combineLatest – the go dutch operator
I call combineLatest
operator the go dutch operator. They are independent and doesn’t wait for each other, they take care of themselves.
Let’s replace the setup code part 3 with the below code:
// 3. We are ready to start printing shirt...
combineLatest(color$, logo$)
.subscribe(([color, logo]) => console.log(`${color} shirt with ${logo}`));
The shirt printing result would be:-
Here is what get to log in the console:
1. white shirt with fish
2. green shirt with fish
3. green shirt with dog
4. red shirt with dog
5. red shirt with bird
6. blue shirt with bird
How does combineLatest
work?
As mentioned, combineLatest
is the go dutch operator – once they meet their mates one time, they will wait for no man. In our case, first function is triggered after both color
and logo
values change. There onwards, either color
or logo
value changed will trigger the log.
1. Ms. Color picks WHITE
2. Mr. Logo picks FISH <- log 01, color + logo first meet, let's go dutch!
3. Ms. Color picks GREEN <- log 02, GREEN + FISH
4. Mr. Logo picks DOG <- log 03, DOG + GREEN
5. Ms. Color picks RED <- log 04, RED + DOG
6. Mr. Logo picks BIRD <- log 05 BIRD + RED
7. Ms. Color picks BLUE <- log 06 BLUE + BIRD
withLatestFrom – the master slave operator
I call withLatestFrom
operator the master slave operator. At first, master must meet the slave. After that, the master will take the lead, giving command. The slave will just follow and has no voice. 🙁
Let’s replace the setup code part 3 with the below code:
// 3. We are ready to start printing shirt...
color$.pipe(withLatestFrom(logo$))
.subscribe(([color, logo]) => console.log(`${color} shirt with ${logo}`));
The shirt printing result would be:-
Here is what get to log in the console:
1. green shirt with fish
2. red shirt with dog
3. blue shirt with bird
How does withLatestFrom
work?
Can you guess who is the master and who is the slave in our case?
You guessed it! color
is the master while logo
is the slave. At first (only once), color
(master) will look for logo
(slave). Once the logo
(slave) has responded, color
(master) will take the lead. Log will get triggered whenever the next color
(master) value is changed. The logo
(slave) value changes will not trigger the console log.
1. Ms. Color picks WHITE <- nothing happen, waiting for slave
2. Mr. Logo picks FISH <- slave found, wait for the master's command
3. Ms. Color picks GREEN <- log 01, master says GREEN! So, GREEN + FISH
4. Mr. Logo picks DOG
5. Ms. Color picks RED <- log 02, master says RED! So, RED + DOG
6. Mr. Logo picks BIRD
7. Ms. Color picks BLUE <- log 03 master says BLUE! So, BLUE + BIRD
forkJoin – the final destination operator
Definitely not the horror movie) kind of final destination! I call forkJoin
operator the final destination operator because they are very serious, they only commit once all parties are very sure that they are completely true, final destination of each other.
Let’s replace the setup code part 3 with the below code:
// 3. We are ready to start printing shirt...
forkJoin(color$, logo$)
.subscribe(([color, logo]) => console.log(`${color} shirt with ${logo}`));
The shirt printing result would be:-
You see it right, the result is NOTHING! There is no log in console.
How does forkJoin
work?
forkJoin
is the final destination
operator! They are very serious to make sure each other are their final destination. In our code, both color
and logo
observables are not complete, we can keep pushing value by calling .next
– that means they are not serious enough and thus they are not final destination
of each other.
So, how do we be serious?
We need to complete
both observables. Let’s replace our setup code part 5 with the below:
// 5. When the two persons(observables) ...
color$.complete();
logo$.complete();
Great! With the above code changes, Here is our shirt printing result:-
Here is what get to log in the console:
1. blue shirt with bird
Here is the sequence of when the log happens:-
1. Ms. Color picks WHITE
2. Mr. Logo picks FISH
3. Ms. Color picks GREEN
4. Mr. Logo picks DOG
5. Ms. Color picks RED
6. Mr. Logo picks BIRD
7. Ms. Color picks BLUE
8. Ms. Color completed <-- color is serious!
9. Mr. Logo completed <--- log no 01, both logo & color are completed. Final destination!
There is more than one way to complete observable. There are operators that allow you to auto complete observable when conditions met, for example take, takeUntil, first.
Let’s say, you only want to make 1 shirt, you only need to know the first color
and logo
, In this case, you don’t care about the rest of the info that Ms. Color & Mr. Logo provide. You can make use of take
or first
operator to achieve auto complete observable once first color
and logo
emit.
Let’s replace the setup code part 3 with the below code:
// 3. We are ready to start printing shirt...
const firstColor$ = color$.pipe(take(1));
const firstLogo$ = logo$.pipe(first());
forkJoin(firstColor$, firstLogo$)
.subscribe(([color, logo]) => console.log(`${color} shirt with ${logo}`));
You can remove all the code in part 5 as well, we don’t need the two lines .complete()
(as previous code) because take
and first
will auto complete the observable when the condition met.
With the above change, you should see a white shirt with fish!
Summary
Phew~ this is a pretty long article huh? Here is the summary of all results.
Let’s wrap up! In summary, these 4 operators trigger the next action (subscribe function in our case) in slightly different conditions:
- zip – the
love birds
, always work as a team, triggers only when all observables return new values - combineLatest – the
go dutch
, start trigger once all observables return new values, then wait for no man, trigger every time when either observable return new value. - withLatestFrom – the
master slave
, master first waits for slave, after that, action get triggered every time only when master return new value. - forkJoin – the
final destination
, trigger once when all observables have completed.
Which operator should I use?
So I guess you can answer “which operator should I use?” better now. As a general rule of thumb – choose the one that works for you. In some cases, the outcome of using different operators might be the same (that’s why people get confused on which one to use), it would be good to understand the intention of the operator & decide accordingly.
One of the most common use case of combination operators would be calling a few apis, wait for all results return, then executing next logic. Either forkJoin
or zip
will work and return same result because api calls are one-time only, auto-completed once result is returned (e.g. Angular httpClient.get
).
However, by understanding the operators more, forkJoin
might be more suitable in this case. It is because we “seriously” want to wait for all http responses to complete before proceed to the next step. zip
is intended for observables with multiple emits. In our case, we expect only one emit for each http request. Therefore, I think forkJoin
is more appropriate (oh well, either way, your code will run just fine & return the same result, but it’s good to know right?).
Demo
Alright, here is the final code. Please note that the code is a little bit different from demo because I include the code to draw the UI. Logic and general code structure stay the same though.
Quiz!
As a bonus, let me give you a quiz!
Figure out the correct results with the provided code. Feel free to play around with it, and explore different scenarios (add more more observables maybe?)!
Quiz Answer: Click here for the quiz’s answer! (please try yourself first)
That’s all. Happy coding!
Source: Scotch.io