Skip to content

zip

The zip() operator in RxJS is a combination operator. Its job is to combine multiple source Observables by waiting for each observable to emit a value at the same index, and then it emits an array containing those values paired together.

Analogy: The Zipper#

Think of a clothing zipper. It has two sides (or more, if you imagine a multi-way zipper!). To close the zipper, teeth from both sides must align at the same position. zip() works the same way:

  1. It subscribes to all the source Observables you provide.
  2. It waits until the first value arrives from every source. It then emits these first values together in an array: [firstValueA, firstValueB, ...].
  3. Then, it waits until the second value arrives from every source. It emits these second values together: [secondValueA, secondValueB, ...].
  4. It continues this process, index by index (0, 1, 2,...).
  5. Crucially: If one source Observable completes before another, zip() will stop emitting new pairs as soon as it runs out of values from the shorter source to pair with. It needs a value from all sources for a given index to emit.

Why Use zip()?#

You use zip() when you have multiple streams and you need to combine their values based on their emission order or index. You specifically want the 1st item from stream A paired with the 1st from stream B, the 2nd with the 2nd, and so on.

Imagine you have two real-time data feeds:

  1. sensorA$ emits temperature readings every second.
  2. sensorB$ emits humidity readings every second, perfectly synchronized with sensor A.

You want to process these readings as pairs (temperature and humidity for the same timestamp/interval). zip is perfect for this.

Another scenario (less common for APIs, more for UI events or other streams): Suppose you want to pair every user click with a corresponding item from another list that gets populated sequentially. The first click pairs with the first item, the second click with the second, etc.

Code Snippet Example#

Let's create a simple Angular component example using zip. We'll zip together values from two simple streams: one emitting letters ('A', 'B', 'C') quickly, and another emitting numbers (10, 20, 30, 40) more slowly.

import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
import { zip, interval, of } from "rxjs";
import { map, take } from "rxjs/operators";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

@Component({
  selector: "app-zip-example",
  standalone: true,
  template: `
    <h2>RxJS zip() Example</h2>
    <p>Combining letters and numbers based on index:</p>
    <ul>
      <li *ngFor="let pair of zippedResult()">{{ pair | json }}</li>
    </ul>
    <p>
      Note: The number stream had '40', but the letter stream completed after
      'C', so zip stopped.
    </p>
  `,
  styles: [
    `
      li {
        font-family: monospace;
      }
    `,
  ],
})
export class ZipExampleComponent implements OnInit {
  private destroyRef = inject(DestroyRef);

  zippedResult = signal<Array<[string, number]>>([]); // Signal to hold the result

  ngOnInit() {
    // Source 1: Emits 'A', 'B', 'C' one after another immediately
    const letters$ = of("A", "B", "C");

    // Source 2: Emits 10, 20, 30, 40 every 500ms
    const numbers$ = interval(500).pipe(
      map((i) => (i + 1) * 10), // Map index 0, 1, 2, 3 to 10, 20, 30, 40
      take(4) // Only take the first 4 values
    );

    // Zip them together
    zip(letters$, numbers$)
      .pipe(
        // zip emits arrays like [string, number]
        takeUntilDestroyed(this.destroyRef) // Auto-unsubscribe
      )
      .subscribe({
        next: (value) => {
          // Update the signal with the latest pair
          // NOTE: For signals, it's often better to collect all results
          // if the stream completes quickly, or update progressively.
          // Here we'll just append for demonstration.
          this.zippedResult.update((current) => [...current, value]);
          console.log("Zipped value:", value);
        },
        complete: () => {
          console.log("Zip completed because the letters$ stream finished.");
        },
        error: (err) => {
          console.error("Zip error:", err);
        },
      });
  }
}

Explanation of the Code:

  1. letters$ emits 'A', 'B', 'C' and then completes.
  2. numbers$ starts emitting 10 (at 500ms), 20 (at 1000ms), 30 (at 1500ms), 40 (at 2000ms).
  3. zip waits:
    • It gets 'A' immediately, but waits for numbers$ to emit.
    • At 500ms, numbers$ emits 10. zip now has the first value from both ('A', 10) -> Emits ['A', 10].
    • It gets 'B' immediately, waits for numbers$.
    • At 1000ms, numbers$ emits 20. zip has the second value from both ('B', 20) -> Emits ['B', 20].
    • It gets 'C' immediately, waits for numbers$.
    • At 1500ms, numbers$ emits 30. zip has the third value from both ('C', 30) -> Emits ['C', 30].
  4. letters$ has now completed. Even though numbers$ emits 40 at 2000ms, zip cannot find a corresponding 4th value from letters$, so it stops and completes.

zip() vs. Other Combination Operators#

  • combineLatest: Emits an array of the latest values from each source whenever any source emits. Doesn't care about index, just the most recent value from all participants.
  • forkJoin: Waits for all source observables to complete, then emits a single array containing the last value emitted by each source. Useful for running parallel one-off tasks (like multiple HTTP requests) and getting all results together at the end.

Use zip() specifically when the order and pairing by index (1st with 1st, 2nd with 2nd, etc.) is what you need.