RxJS - reusing an observable which has completed - rxjs

Link to code in stackblitz
Is there a way to repeat a completed observable multiple times?
Say I have a button that on click creates an interval observable that emits 10 values, then completes:
fromEvent(button, 'click').pipe(
switchMapTo(interval(500)),
takeWhile(i => i < 10)
)
In the subscription I want to handle both the next and the complete methods:
.subscribe(
i => console.log(i),
() => {},
() => console.log('completed')
);
In this way - the first click will emit one sequence and once it completes, subsequent clicks will not emit again. Is there any way to write this so that all clicks emit the sequence?

I think you should complete the inner observable and not the whole sequence.
(takeWhile should be piped to interval);
You should use switchMap only if you are happy to dismiss the old sequence once a new click event comes in. mergeMap or concatMap otherwise.
const sequence = () => interval(500).pipe(take(10));
fromEvent(button, 'click').pipe(
switchMap(sequence),
)

Related

Repeat on an BehaviorSubject

I want to reemit the last value of my observable at a fix interval, to I tried
obs.pipe(repeat({delay:1000})).subscribe(x => console.log('Emitted', x));
but it did not work. after looking into this, my observable is in fact a BehaviorSubject.
So my Question is Why does the 1st emits every second
of('Observable').pipe(repeat({ delay: 1000 })).subscribe(x => console.log(x));
but not the this?
var bs = new BehaviorSubject('BehaviorSubject');
bs.pipe(repeat({ delay: 1000 })).subscribe(x => console.log(x));
How to do it with my BehaviorSubject?
Edit
And I would also like to reset my timer when the subject emits a new value.
the solution I found is
var bs = new BehaviorSubject('BehaviorSubject');
bs.pipe(switchMap(x => timer(0,1000).pipe(map => () => x)).subscribe(x => console.log(x));
but it feels ugly.
You can derive an observable from your BehaviorSubject that switchMaps to a timer that emits the received value. Whenever the subject emits, the timer is reset and will emit the latest value:
const bs = new BehaviorSubject('initial value');
const repeated = bs.pipe(
switchMap(val => timer(0, 1000).pipe(
map(() => val)
))
);
Here's a StackBlitz demo.
So my Question is Why does the 1st emits every second, but not the this?
The reason your example code using of as the source works and not the code using the BehaviorSubject can be found in the documentation of the repeat operator:
Returns an Observable that will resubscribe to the source stream when the source stream completes.
The observable created using of completes after it emits the provided value, so it will resubscribe. Since the BehaviorSubject was not completed, it will not resubscribe.

How do I create an observable which emits a value from an ngrx selector, then another value after a delay?

Given an ngrx selector:
store.select('count')
I want to create an observable that will emit values emitted by the selector, then emit another specific value after a delay.
Using concat doesn't work as (I assume) the selector doesn't complete, so the 0 is never emitted:
this.count$ = concat(store.select('count'), of(0).pipe(delay(2000)));
Stackblitz: https://stackblitz.com/edit/so-selector-delay?file=src/app/my-counter/my-counter.component.ts
- click 'Increment' button - Current Count should change to 1 then back to 0 after 2 seconds.
If you want to emit the store.select('count') value, then essentially reset it to 0 after not receiving an emission for 2 seconds, you can use a switchMap to create a source that emits two values:
The emitted count
The "default" value of 0 after 2000ms
The trick here is that the second value (0) will NOT be emitted if another store.select('count') value is received, because switchMap will create a new source and "switch" to that:
this.count$ = store.select('count').pipe(
switchMap(count => concat(
of(count),
of(0).pipe(delay(2000))
))
);
Here's a working StackBlitz demo.
It might even be worth creating a custom operator:
this.count$ = this.store.select('count').pipe(
resetAfterDelay(0, 2000)
);
export function resetAfterDelay<T>(defaultValue: T, delayMs: number) {
return switchMap((value: T) => concat(
of(value),
of(defaultValue).pipe(delay(delayMs))
));
}
StackBlitz
Below is an approach using combineLatest and BehaviorSubject
We are hold a value in a subject and create a timer that emits value 0 after 2s. So we have two Observables, one emits immediately and the other after 2s. We combine this two and the effect is a single observable as desired
valueHolderSubject$ = new BehaviorSubject(0);
...
this.count$ = combineLatest([
store.select("count").pipe(
tap(x => this.valueHolderSubject$.next(x)),
tap(() =>
timer(2000).subscribe({
next: () => {
this.valueHolderSubject$.next(0);
}
})
),
map(() => this.valueHolderSubject$.value),
distinctUntilChanged()
),
this.valueHolderSubject$
]).pipe(map(([, x]) => x));
Demo Here
A per my comments on the answer from BizzyBob, I was getting unreliable results. But I refined it to work using:
this.count$ = merge(
store.select('count'),
store.select('count').pipe(delay(2000), map(_ => 0))
);
See stackblitz: https://stackblitz.com/edit/so-selector-delay-merge-working?file=src/app/my-counter/my-counter.component.ts

Replace Observable which already has subscribers with other Observable

I have an Observable which gets later gets "replace" with another Observable.
How can I swap the Observable without loosing my subscribers?
const source = NEVER
const source2 = interval(1000);
source.subscribe(x => console.log(x));
// source.switch(source2)
source.switch(source2) is obviously not a valid operation. But it demonstrates, what I'dlike to achieve.
Same example on StackBlitz:
https://stackblitz.com/edit/rxjs-76a7ew
What would I need to do after the subscribtion, so this code will start printing the numbers from interval?
so you want switch to source2
source.pipe(
switchMap(() => source2)
).subscribe(x => console.log(x)); // x here is source2
you could use mergeMap or concatMap as well but I would recommend to use switchMap in this case as it's going to cancel the previous emit

RxJS Observables and a wheel event

I was wondering if anyone could help me theorize a solution. I create an Observable from a wheel event, prevent the default action, throttle it by 200ms, map deltaY (which I can use to determine direction), and then I share it.
My problem is that it emits more values than I need creating a situation where my subscribers continue to fire even after the desired action has occurred. I'm new to RxJS so bear with me but... Is there a way for me to take the "first" value emitted in a series of values within say X amount of time passed and not have the observable complete?
Below is the code.
import { fromEvent } from 'rxjs';
const wheel$ = fromEvent(document, 'wheel')
.pipe(
tap((event) => event.preventDefault()),
// throttleTime(200), /* I have tried throttling and debouncing but that doesn't work - values will continue to be emitted */
map((event) => event.deltaY),
share()
)
// handles scrolling down //
wheel$.pipe(filter((val) => val > 0))
.subscribe((event) => {
if (this.props.isScrolling) return
this.scrollDown();
})
One solution would be "bufferCount()"
of(1,2,3,4,5,6,7,8,9).pipe(
bufferCount(3)
).subscribe(data => console.log(data) )
would create packages of 3 signals. So the Events would be
[1,2,3]
[4,5,6]
[7,8,9]
Or "throttleTime(xy)", than it will put the first signal through, will ignore for "xy" milliseconds every other signal, and then give the next signal a chance.
interval(500).pipe(
throttleTime(2000)
).subscribe(data => console.log(data) )
will result in something like
1 // ignore everything the next 2 seconds
5 // ignore everything the next 2 seconds
9 // ignore everything the next 2 seconds
...
warm regards
Found this question while looking for a solution.
I went with a switchMap
fromEvent(el, 'wheel').pipe(
switchMap(e =>
concat(
//wrap the original event, which fires immediately
of({kind: 'ON_WHEEL', e}),
// fire a onWheelEnd after a 200ms delay
// this event is unsubscribed by the switchMap if a new event comes in
// before the delay
of({kind: 'ON_WHEEL_END}).pipe(delay(200))
)
)
)

why concatMap is not subscribing to all groupBy Observables rxjs?

I have a question why is this not writing to console the numbers 2,4,6? what is the explanation?
Observable.range(1, 6)
.groupBy(n => n % 2 === 0)
.concatMap(obs => obs)
.subscribe((n) => console.log(n), null, () => console.log('complete concatMap'))
// this is the output
1 -
3 -
5 -
complete concatMap
The basic problem is that you're using concatMap that subscribes to the next Observable only when the previous one completed. groupBy emits two GroupedObservables so it subscribes to the first one and I think before it can subscribe to the second one the chain completes. This means the observer receives the complete notification from the first GroupedObservable and therefore you never see values from the second GroupedObservable (to be honest I'm not 100% sure it really happens like this but that makes sense to be without further investigating you example).
So if you want only the second group you could do:
import { Observable } from 'rxjs';
Observable.range(1, 6)
.groupBy(n => n % 2 === 0)
.filter(o => o.key === true)
.concatMap(obs => obs)
.subscribe((n) => console.log(n), null, () => console.log('complete concatMap'))
See live demo (open console): https://stackblitz.com/edit/rxjs5-sfused
I checked the source code and groupBy completes all groups after receiving the complete notification (which it does after receiving all values from range) and therefore there's never space for concatMap to subscribe to the second Observable.
See this: https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/groupBy.ts#L200-L210
Problem is that groupBy operator emits Subjects for each key.
Concat map subscribes to subsequent Subject only after the first one is completed, i.e. it misses a chance to catch items from subsequent Subject because all of the sub-streams emit values in the same time.
Kudos to: https://blog.angularindepth.com/those-hidden-gotchas-within-rxjs-7d5c57406041
TL;DR:
GroupBy receives subjectSelector as a 4th argument. You can use it to force using ReplaySubject instead of Subject (default).
Observable.range(1, 6)
.groupBy(
n => n % 2 === 0,
null,
null,
() => new ReplaySubject() // <-- Here we go
)
.concatMap(obs => obs)
.subscribe((n) => console.log(n))
Demo on RxViz

Resources