RxJS iif arguments are called when shouldn't
Asked Answered
A

4

25

I want to conditionally dispatch some actions using iif utility from RxJS. The problem is that second argument to iif is called even if test function returns false. This throws an error and app crashes immediately. I am new to to the power of RxJS so i probably don't know something. And i am using connected-react-router package if that matters.

export const roomRouteEpic: Epic = (action$, state$) =>
  action$.ofType(LOCATION_CHANGE).pipe(
    pluck('payload'),
    mergeMap(payload =>
      iif(
        () => {
          console.log('NOT LOGGED');
          return /^\/room\/\d+$/.test(payload.location.pathname); // set as '/login'
        },
        merge(
          tap(v => console.log('NOT LOGGED TOO')),
          of(
            // following state value is immediately evaluated
            state$.value.rooms.list[payload.location.pathname.split('/')[1]]
              ? actions.rooms.initRoomEnter()
              : actions.rooms.initRoomCreate(),
          ),
          of(actions.global.setIsLoading(true)),
        ),
        empty(),
      ),
    ),
  );
Addia answered 8/1, 2019 at 18:54 Comment(3)
The second argument to iif is SUPPOSED to be called if the test function returns false. The first argument will be called if the test function returns true. See the docs hereUbangi
By first argument i count test function. Second and third are called after test.Addia
Ok, thanks for clarifying. Next, double check that the test function actually returns true. If it returns anything other than that, then iif will evaluate it as false. I would try something like replacing what you have currently with () => true and see if it is indeed the iif causing the issue, or your test logic.Ubangi
A
8

Ok, i found an answer on my own. My solution is to remove iif completely and rely on just ternary operator inside mergeMap. that way its not evaluated after every 'LOCATION_CHANGE' and just if regExp returns true. Thanks for your interest.

export const roomRouteEpic: Epic = (action$, state$) =>
  action$.ofType(LOCATION_CHANGE).pipe(
    pluck<any, any>('payload'),
    mergeMap(payload =>
      /^\/room\/\d+$/.test(payload.location.pathname)
        ? of(
            state$.value.rooms.list[payload.location.pathname.split('/')[2]]
              ? actions.rooms.initRoomEnter()
              : actions.rooms.initRoomCreate(),
            actions.global.setIsLoading(true),
          )
        : EMPTY,
    ),
  );
Addia answered 9/1, 2019 at 8:6 Comment(0)
M
80

A little late to the party, but I found that the role of iif is not to execute one path over the other, but to subscribe to one Observable or the other. That said, it will execute any and all code paths required to get each Observable.

From this example...

import { iif, of, pipe } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

const source$ = of('Hello');
const obsOne$ = (x) => {console.log(`${x} World`); return of('One')};
const obsTwo$ = (x) => {console.log(`${x}, Goodbye`); return of('Two')};

source$.pipe(
  mergeMap(v =>
    iif(
      () => v === 'Hello',
      obsOne$(v),
      obsTwo$(v)
    ))
).subscribe(console.log);

you'll get the following output

Hello World
Hello, Goodbye
One

This is because, in order to get obsOne$ it needed to print Hello World. The same is true for obsTwo$ (except that path prints Hello, Goodbye).

However you'll notice that it only prints One and not Two. This is because iif evaluated to true, thus subscribing to obsOne$.

While your ternary works - I found this article explains a more RxJS driven way of achieving your desired outcome quite nicely: https://rangle.io/blog/rxjs-where-is-the-if-else-operator/

Mastiff answered 31/5, 2019 at 18:53 Comment(4)
Great explanation on iif() execution.Hochstetler
Great explanation! There is a way around this, by using defer. So if you do iif( condition, defer(() => obsOne$), defer(() => obsTwo$)) only the path corresponding to the condition will get executed.Carlyn
@SamuelBushi thanks! But its acctually iif(() => condition, defer(() => obsOne$), defer(() => obsTwo$)Palmation
Even later to the party. But this isn't so much about Observables and iif but more about what iif actually is. It's just a function. When a function is called, it needs all its arguments. When one of those arguments is itself a function-invocation - then the function must be invoked. Hence why obsOne$() and obsTwo$() are always called.Incumber
A
8

Ok, i found an answer on my own. My solution is to remove iif completely and rely on just ternary operator inside mergeMap. that way its not evaluated after every 'LOCATION_CHANGE' and just if regExp returns true. Thanks for your interest.

export const roomRouteEpic: Epic = (action$, state$) =>
  action$.ofType(LOCATION_CHANGE).pipe(
    pluck<any, any>('payload'),
    mergeMap(payload =>
      /^\/room\/\d+$/.test(payload.location.pathname)
        ? of(
            state$.value.rooms.list[payload.location.pathname.split('/')[2]]
              ? actions.rooms.initRoomEnter()
              : actions.rooms.initRoomCreate(),
            actions.global.setIsLoading(true),
          )
        : EMPTY,
    ),
  );
Addia answered 9/1, 2019 at 8:6 Comment(0)
E
1

If you use tap operator inside observable creation(because it returns void), it will cause error as below

Error: You provided 'function tapOperatorFunction(source) {
return source.lift(new DoOperator(nextOrObserver, error, complete));
}' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

Remove the tap and put the console in the subscribe().

I have created a stackblitz demo.

Emrich answered 9/1, 2019 at 5:18 Comment(0)
V
1

Another consideration is that even though Observables and Promises can be used in the same context many times when working with RxJS, their behavior will be different when dealing with iif. As mentioned above, iif conditionally subscribes; it doesn't conditionally execute. I had something like this:

.pipe(
    mergeMap((input) =>
        iif(() => condition,
            functionReturningAPromise(input),  // A Promise!
            of(null)
        )
    )
)

This was evaluating the Promise-returning function regardless of the condition because Promises don't need to be subscribed to to run. I fixed it by switching to an if statement (a ternary would have worked as well).

Varanasi answered 7/10, 2022 at 18:15 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.