rxjs conditional startWith , endWith using iif operator - rxjs

Still figuring rxjs out and its a great library (have to admit). Given that we have this source and booleans.
let source = [0,1,2,3,4,5,6,7,8,9];
let swEnabled = false;
let ewEnabled = false;
And we would like to conditionally enable startwith and endwith operators based on swEnabled and ewEnabled to the source.....
source.pipe(
iif(() => swEnabled,startWith(1000),),
iif(() => ewEnabled,endWith(1000),),
).subscribe(
(n)=>{
console.log(n);
});
but no joy..... anyone can provide an example of this? Maybe the approach is wrong. Or can you suggest some alternative approach ?
Thanks in advance

You can conditionally build an array of operators as follows:
import { MonoTypeOperatorFunction, of } from 'rxjs';
import { pipeFromArray } from 'rxjs/internal/util/pipe';
import { endWith, startWith } from 'rxjs/operators';
let source = of(0,1,2,3,4,5,6,7,8,9);
let swEnabled = false;
let ewEnabled = false;
let operators: MonoTypeOperatorFunction<number>[] = []
if (swEnabled) {
operators.push(startWith(1000))
}
if (ewEnabled) {
operators.push(endWith(1000))
}
source.pipe(pipeFromArray(operators)).subscribe(e => console.log(e))
There is currently an open issue about not being able to use the spread syntax inside the pipe. If that issue gets fixed, then you will no longer need the pipeFromArray function and the solution can be simplified to source.pipe(...operators).
Re iif: Note that the iif function is an Observable constructor function and not an operator function. That means that you cannot use iif as an operator inside pipe() as you have shown. By observing the RxJx reference page, you can see that there are two sections, amongst others, titled index and operators. Only the functions under operators can be used directly within the pipe(). The functions under index can be used to construct source Observables.
Re startWith and endWith: These two functions are operator functions (meant to be used inside pipe) and not Observables. The iif function expects an Observable as the second and third argument.

I was having the same question, but after trying different strategies I came up with this solution:
someObservable.pipe(
// anyOperator(),
condition ? conditionalOperator() : tap(() => {})
)
In this case, if condition is true, operatorA is added to pipe, if not, the tap(() => {}) is like a "noop" (anyone can mix the condition and parameters to fit any needs).

Related

Is it bad to use a variable from outside the observable pipe within an operator?

Is using a variable from outside an observable within an operator considered a (significantly) bad practice?
createObservableExample1(parameter1: string, obs$: Observable<string>): Observable<string> {
return obs$.pipe(
map( x => {
const returnValue = `${parameter1}, ${x}`;
return returnValue;
})
);
}
I understand you can do something like this:
createObservableExample2(parameter1: string, obs$: Observable<string>): Observable<string> {
return combineLatest([
of(parameter1),
obs$
]).pipe(
map( (x, y) => {
const returnValue = `${x}, ${y}`;
return returnValue;
})
);
}
But is it worth it?
Does this just come down to accessing variables from outside the scope of anonymous function? Would this force the context of the enclosing method to exist for longer than it should? I remember a code tool I used to use for C# complaining about something similar to this. I have found somewhat related topics by searching for, "anonymous functions and closures", but as of yet, nothing really discussing the scenario explained above.
I ask because I have been creating some relatively complex observables that have enormous operator chains, and constantly adding the needed variables, using combineLatest and of, from the parent scope can make the code even harder to follow.
When I teach Reactive programming to neophytes, I try to make them grasp : Do not break the reactivity by having uneccessary side effects :
no input that from a state (for example using a class or instance property
no storing outside value.
There is none of these red flags in your example. Your function is pure & idempotent with both implementation, go with what ever you like and if possible be consistant within your code base !

RxJs share operator and Observable created with range

I am trying to understand why share RxJs operator works differently if the source Observable is created with range instead of timer.
Changing the original code to:
const source = range(1, 1)
.pipe(
share()
)
const example = source.pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***'),
)
const sharedExample = example
const subscribeThree = sharedExample.subscribe(val => console.log(val))
const subscribeFour = sharedExample.subscribe(val => console.log(val))
Results in:
console.log src/pipeline/foo.spec.ts:223
SIDE EFFECT
console.log src/pipeline/foo.spec.ts:228
RESULT
console.log src/pipeline/foo.spec.ts:223
SIDE EFFECT
console.log src/pipeline/foo.spec.ts:229
RESULT
Basically, the side effect is invoked more than once.
As far as I know range is supposed to be a cold observable but it is said that share should turn cold observables to hot.
What is the explanation behind this behaviour ?
Two things to point out.
First, if you look closely at the function signature for range, you'll see it takes a third parameter, a SchedulerLike.
If unspecified, RxJS calls the next handler of each subscriber immediately with the relevant value for the range observable until it's exhausted. This isn't desirable if you intend to use the share operator, because it effectively bypasses any shared side effect processing that might be introduced.
Relevant snippet taken from the actual implementation:
// src/internal/observable/range.ts#L53
do {
if (index++ >= count) {
subscriber.complete();
break;
}
subscriber.next(current++);
if (subscriber.closed) {
break;
}
} while (true);
timer also takes an optional SchedulerLike argument. If unspecified, the implementation adopts AsyncScheduler by default, different to the default for range.
Secondly, the share operator should follow all other operators that might have side effects. If it precedes them, the expected unifying behaviour of pipe operator processing is lost.
So, with both points in mind, to make the share operator work with range as you're expecting:
const { asyncScheduler, range, timer } = rxjs;
const { mapTo, tap, share } = rxjs.operators;
// Pass in an `AsyncScheduler` to prevent immediate `next` handler calls
const source = range(1, 1, asyncScheduler).pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***'),
// All preceding operators will be in shared processing
share(),
);
const sub3 = source.subscribe(console.log);
const sub4 = source.subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>

RxJS iif arguments are called when shouldn't

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(),
),
),
);
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/
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,
),
);
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.
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).

Rx.Observable validate items in stream (pass or throw)

Given an Stream within an Observable, I want to validate/check each item. In case one is broken I want to throw an error via Observable.throw, hence break all further processing.
My clunky solution would be
import * as Rx from 'rxjs'
inputStream.mergeMap(item => (isValid(item))
? Rx.Observable.of(item)
: Rx.Observable.throw(new Error("not valid"))
)
This seems ugly, as it constructs for the positive flow a bunch of unnecessary Observables.
Is there a better way to check items in an Observable?
You can use just normal map and throw an exception inside it:
inputStream.map(item => {
if (isValid(item)) {
return item;
}
throw new Error("not valid");
})
If I understand your question well, you can use takeWhile operator to do this. For example;
yourObservable.takeWhile(item => {
//your condition
}).subscribe(i => console.log(i));
It just takes values while your expression is true. When it becomes false, it stops.
You can learn more from here. Also you can check this page out. I hope it helps!

RxJS tricky zip two observables

Having getLastOrder() and getUserDetails(lastOrder.owner) async operations. (both returns Observable)
Looking for fancy operator to get combined result that will include both: [lastOrder, userDetails] who owns that order.
P.S. doing that via temporary variables does not look pretty
Not sure about fancy operators, but this can be solved quite easily by combining the operators flatMap and map. Also note that my functions just return dummy data but should work the same way with any observables:
function getLastOrder() {
return Rx.Observable.of({owner: 'foo'})
}
function getUserDetails(owner) {
return Rx.Observable.of({details: 'bar', owner})
}
function combined() {
return getLastOrder()
.flatMap(order => getUserDetails(order.owner).map(details => [order, details]))
}
When you think about prettyness, also think about the next guy who will read it.

Resources