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$.

you, ms. color & mr. 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:-

sequence of info

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:-

zip - printed shirts

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:-

combinedLatest - printed shirts

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:-

withLatestFrom - printed shirts

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:- forkJoin - printed shirts

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:-

forkJoin (complete) - printed shirts

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!

forkjoin (auto complete) - printed shirtst

Summary

Phew~ this is a pretty long article huh? Here is the summary of all results. one page answer

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.

See the Demo on Stackblitz

Quiz!

As a bonus, let me give you a quiz!

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!