#JavaScript
Recursion in RxJS
The requirement: after a user selects or photographs an image, send it to an image-recognition API. The API rejects images over a certain size and returns…
Aug 1, 2024
The Mystery Behind RxJS iif
A common pattern in business logic: branch on a precondition to decide which API to call. For order payment, if it's a new order call the create-order…
Jun 22, 2024
The Mystery Behind RxJS iif

The Mystery Behind RxJS iif

June 22, 2024

A common pattern in business logic: branch on a precondition to decide which API to call. For order payment, if it's a new order call the create-order endpoint; if the order already exists call the re-pay endpoint. The natural instinct is to reach for iif:

processOrder$
  .pipe(
    switchMap((isNewOrder) =>
      iif(
        () => isNewOrder === true,
        from(fetch("/order")),
        from(fetch("/pay")),
      ),
    ),
  )
  .subscribe();

Watching the network tab reveals something surprising — both requests fire, regardless of the condition.

network_request.webp

Digging into iif

export function iif<T, F>(
  condition: () => boolean,
  trueResult: ObservableInput<T>,
  falseResult: ObservableInput<F>,
): Observable<T | F> {
  return defer(() => (condition() ? trueResult : falseResult));
}

iif is just a thin wrapper around defer. What does defer do?

export function defer<R extends ObservableInput<any>>(
  observableFactory: () => R,
): Observable<ObservedValueOf<R>> {
  return new Observable<ObservedValueOf<R>>((subscriber) => {
    from(observableFactory()).subscribe(subscriber);
  });
}

defer creates an Observable that defers subscription until the factory function is called at subscribe-time — similar to switchMap.

The bug: iif is a function. When you pass from(fetch("/order")) and from(fetch("/pay")) as arguments, JavaScript evaluates them before iif is even called. Both fetch calls fire immediately at argument-evaluation time; iif then just selects which already-fired Observable to subscribe to.

The Fix

The simplest fix: use a ternary or if/else to avoid eagerly evaluating the branch you don't need:

processOrder$.pipe(
  switchMap((isNewOrder) =>
    isNewOrder === true ?
    from(fetch('/order')) :
    from(fetch('/pay')),
  )),
).subscribe();

If you want to keep iif for its expressiveness, wrap each branch in defer so the inner Observable is only created at subscribe-time:

of(true)
  .pipe(
    switchMap((result) =>
      iif(
        () => result === true,
        defer(() => from(fetch("/pay"))),
        defer(() => from(fetch("/order"))),
      ),
    ),
  )
  .subscribe();

Both defer calls are still invoked as iif arguments — but the from(fetch(...)) inside each is not executed until the defer Observable is subscribed to. Only the winning branch ever gets subscribed, so only one request fires.

The Mystery Behind RxJS iif | Cheng's Blog