14
votes

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(),
      ),
    ),
  );
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 here - dmcgrandle
By first argument i count test function. Second and third are called after test. - E1-XP
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

3 Answers

42
votes

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/

4
votes

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,
    ),
  );
1
votes

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.