Skip to the solution
HardFrontend11 min read

What is the difference between forkJoin, zip, combineLatest, and withLatestFrom?

473 views

The problem

Question presented to candidate: "A dashboard needs the user and their permissions before it renders, a product list re-filters whenever the search text or the category changes, and a Save button sends the current filters. Which RxJS combination operators would you use, and how do forkJoin, zip, combineLatest and withLatestFrom differ?"

What a strong answer should cover:

  • forkJoin waits for every source to complete, then emits once with each source's last value: the RxJS Promise.all, ideal for parallel HTTP calls. If any source completes empty it emits nothing; if any errors, it errors.
  • zip pairs values by index (1st with 1st, 2nd with 2nd) and buffers the faster source.
  • combineLatest waits until each source has emitted once, then emits the latest of all on every emission from any source: derived state from several inputs.
  • withLatestFrom is an operator: only the source triggers an emission; the other streams are sampled, and source values that arrive before the others have a value are dropped.
  • Practical differences: completion behaviour, the object/array forms (forkJoin({ a, b })), deprecated rest-argument signatures, and that computed() often replaces combineLatest for signal state.

Clarifying questions expected:

  • "Do the sources complete (HTTP calls) or are they long-lived (form values, store selectors)?"
  • "Should every source be able to trigger an update, or only one of them?"

Code / implementation expected: Yes β€” a timeline or small component that shows when each operator emits for the same inputs.

  • rxjs
  • operators

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read Β· 10 sections

Read the code

AngularPage load with forkJoin, live filters with combineLatest, Save with withLatestFrom
import { Component, OnDestroy, signal } from "@angular/core";
import { BehaviorSubject, Subject, Subscription, combineLatest, delay, forkJoin, of, withLatestFrom } from "rxjs";

// Fake HTTP calls: each emits once after a delay and completes, like HttpClient.
const getUser = () => of({ name: "Ann" }).pipe(delay(60));
const getPermissions = () => of(["read", "write"]).pipe(delay(20));

const PRODUCTS = ["Apple", "Avocado", "Banana", "Blueberry", "Carrot"];

@Component({
  selector: "app-root",
  template: `
    <p>Page load (forkJoin): {{ header() }}</p>
    <input placeholder="search" (input)="search$.next($any($event.target).value)" />
    <select (change)="startsWith$.next($any($event.target).value)">
      <option value="">any letter</option>
      <option value="A">A</option>
      <option value="B">B</option>
    </select>
    <p>Results (combineLatest): {{ results().join(", ") }}</p>
    <button (click)="save$.next()">Save filters</button>
    <p>Saved (withLatestFrom): {{ saved() }}</p>
  `,
})
export class AppComponent implements OnDestroy {
  private subs = new Subscription();
  search$ = new BehaviorSubject("");
  startsWith$ = new BehaviorSubject("");
  save$ = new Subject<void>();

  header = signal("loading...");
  results = signal<string[]>([]);
  saved = signal("nothing yet");

  constructor() {
    // forkJoin: wait for BOTH requests to complete, then emit once with the last value of each.
    this.subs.add(
      forkJoin({ user: getUser(), perms: getPermissions() }).subscribe(({ user, perms }) =>
        this.header.set(user.name + " can " + perms.join(" + ")),
      ),
    );
    // combineLatest: re-run whenever EITHER filter changes (both must have a value; BehaviorSubject gives one).
    this.subs.add(
      combineLatest([this.search$, this.startsWith$]).subscribe(([q, letter]) =>
        this.results.set(
          PRODUCTS.filter((p) => p.toLowerCase().includes(q.toLowerCase()) && p.startsWith(letter)),
        ),
      ),
    );
    // withLatestFrom: only a click triggers; the filters are sampled, not listened to.
    this.subs.add(
      this.save$
        .pipe(withLatestFrom(this.search$, this.startsWith$))
        .subscribe(([, q, letter]) => this.saved.set(JSON.stringify({ q, letter }))),
    );
  }

  ngOnDestroy() {
    this.subs.unsubscribe();
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 103 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

How do you dynamically create components?

Continue