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