23

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(),
      ),
    ),
  );
E1-XP
  • 363
  • 1
  • 3
  • 10
  • 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 [here](https://github.com/ReactiveX/rxjs/blob/master/src/internal/observable/iif.ts) – dmcgrandle Jan 08 '19 at 19:31
  • By first argument i count test function. Second and third are called after test. – E1-XP Jan 08 '19 at 19:37
  • 1
    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. – dmcgrandle Jan 08 '19 at 20:20

4 Answers4

73

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/

Roy
  • 7,811
  • 4
  • 24
  • 47
Sai
  • 731
  • 5
  • 2
  • 9
    Great explanation on `iif()` execution. – Homer Aug 29 '19 at 20:00
  • 8
    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. – Samuel Bushi Aug 04 '21 at 23:37
  • @SamuelBushi thanks! But its acctually `iif(() => condition, defer(() => obsOne$), defer(() => obsTwo$)` – Leonardo Rick Sep 27 '22 at 17:22
  • 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. – El Ronnoco Aug 17 '23 at 15:17
7

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,
    ),
  );
E1-XP
  • 363
  • 1
  • 3
  • 10
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.

KiraAG
  • 773
  • 4
  • 19
0

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

Carcigenicate
  • 43,494
  • 9
  • 68
  • 117