Necessity of the share() operator in a RxJs pipeline - rxjs

I am playing with a sample "save indicator" that relies on RxJS.
Here is the full source code:
import { fromEvent, of, merge, empty, concat, defer } from 'rxjs';
import {
delay,
map,
mergeMap,
tap,
debounceTime,
distinctUntilChanged,
mapTo,
filter,
share,
switchAll
} from 'rxjs/operators';
import { format } from 'date-fns';
​
// track in progress saves
let savesInProgress = 0;
​
// references
const input = document.getElementById('note-input');
const saveIndicator = document.querySelector('.save-indicator');
​
// streams
const keyup$ = fromEvent(input, 'keyup');
​
// fake save request
const saveChanges = value => {
return of(value).pipe(delay(1500));
};
​
/**
* Trigger a save when the user stops typing for 200ms
* After new data has been successfully saved, so a saved
* and last updated indicator.
*/
const inputToSave$ = keyup$.pipe(
debounceTime(200),
map(e => e.target.value),
distinctUntilChanged(),
share()//Here!!
);
​
const savesInProgress$ = inputToSave$.pipe(
mapTo(of('Saving')),
tap(_ => savesInProgress++)
);
​
const savesCompleted$ = inputToSave$.pipe(
mergeMap(saveChanges),
tap(_ => savesInProgress--),
// ignore if additional saves are in progress
filter(_ => !savesInProgress),
mapTo(
concat(
// display saved for 2s
of('Saved!'),
empty().pipe(delay(2000)),
// then last updated time, defer for proper time
defer(() => of(`Last updated: ${format(Date.now(), 'MM/DD/YYYY hh:mm')}`))
)
)
);
​
merge(savesInProgress$, savesCompleted$)
.pipe(
/*
If new save comes in when our completion observable is running, we want to switch to it for a status update.
*/
switchAll()
)
.subscribe(status => {
saveIndicator.innerHTML = status;
});
And the link where the source code can be found: https://www.learnrxjs.io/learn-rxjs/recipes/save-indicator
I am wondering why the share() multicast operator is necessary here. I have tried removing it and the app's behavior is unchanged.
Can someone please explain why the share() operator is required here?
edit: Please also bear in mind that there is only one subscription/subscribe.

Related

Rxjs - cancel debounce in the specific case

Question about rxjs puzzle.
I have the input observable stream and it will emit after 3 secs when I type some.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
// input is HTMLInputElement
const input$ = fromEvent(input, "input");
input$
.pipe(debounce(() => interval(3000)))
.subscribe(e => console.log(e.target.value));
I would like to make a change to cancel the debounce and emit immediately once the button is clicked. But, if I don't click the button, it will wait 3 secs.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
const input$ = fromEvent(input, "input");
// add click observable stream
const click$ = fromEvent(button, "click");
input$
.pipe(debounce(() => interval(3000)))
// I can't get this to work in the mix!!
// .pipe(debounce(() => click$))
.subscribe(e => console.log(e.target.value));
How can this be achieved?
sounds like a race operator.
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
input$
.pipe(
switchMap(value => race(
click$,
timer(3000),
).pipe(
take(1),
mapTo(value),
)),
.subscribe(e => console.log(e.target.value));
Here is the solution to toggle debounce, what you have to do is to convert interval() to a stream that change interval time base on button click
Js
import { fromEvent, interval,timer} from 'rxjs';
import { debounce,scan,shareReplay,map,startWith,tap,switchMap} from 'rxjs/operators';
const input = fromEvent(document.getElementById('text'), 'input');
const debounceToggle=fromEvent(document.getElementById('toggle'),'click').pipe(
scan((acc,curr)=>!acc,false),
map(on=>on?0:3000),
startWith(3000),
shareReplay(1),
switchMap(value=>interval(value))
)
const result = input.pipe(debounce(() => {
return debounceToggle
}));
result.subscribe(x => console.log(x.target.value));
HTML
<button id="toggle">toggle debounce</button>
<input type="text" id="text"/>
Here could be another solution I think:
input$
.pipe(
debounce(
() => interval(3000).pipe(takeUntil(buttonClick$))
)
)
.subscribe(e => console.log(e.target.value));
debounce will emit the value that caused the inner observable's subscription, when it either completes/emits a value
// Called when the inner observable emits a value
// The inner obs. will complete after this as well
notifyNext(outerValue: T, innerValue: R,
outerIndex: number, innerIndex: number,
innerSub: InnerSubscriber<T, R>): void {
this.emitValue();
}
// Called when the inner observable completes
notifyComplete(): void {
this.emitValue();
}
Source code
The following would be the simplest in my opinion:
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
merge(
input$.pipe(debounceTime(3000)),
click$
).pipe(
map(() => input.value)
).subscribe(val => console.log(val));
https://stackblitz.com/edit/rxjs-8bnhxd
Also, you are essentially "combining" 2 different events here, it doesn't make sense to me to rely on event.target.value, as it could be referring to different things which makes it hard to read.

Don't emit until some other observable has emitted, then emit all previous values

I'm looking for a way to buffer values of an observable until some other observable has emitted, but then emit all the previous values. Something like skipUntil, if skipUntil also emitted skipped values as soon as the second observable emitted.
--a--b----c-----d---e--- (source)
-----------1------------- (other1)
------------abc-d---e----(desired output)
You can use bufferWhen:
import { fromEvent, interval } from 'rxjs';
import { bufferWhen } from 'rxjs/operators';
const clicks = fromEvent(document, 'click');
const buffered = clicks.pipe(bufferWhen(() =>
interval(1000 + Math.random() * 4000)
));
buffered.subscribe(x => console.log(x));
Here's the custom operator I came up with. Not sure if it can be done in a prettier way.
export function bufferUntil(stopBufferSignal: Observable<any>) {
return <T>(source: Observable<T>): Observable<T> => {
return source.pipe(buffer(stopBufferSignal),
take(1),
flatMap(ar => {
const sourceWithNSkipped$: Observable<T> = source.pipe(skip(ar.length));
const bufferedItems$: Observable<T> = from(ar);
return bufferedItems$.pipe(concat(sourceWithNSkipped$))
}));
}
}

how to trigger something immediately then debounce

I have an observable bound to event keyUp on an input box.
For each key pressed. I want to console.log 'Do something now'.
And if there's no key pressed for 5 seconds, then I want to console.log 'Do something else'
import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';
const searchBox = document.getElementById('search');
const keyup$ = fromEvent(searchBox, 'keyup')
keyup$.pipe(
switchMap((i: any) => 'doSomethingNow'), // I use switchMap here because 'doSomethingNow' is an http request in my real code so that on each key pressed, it cancels the previous http request if it was not finished and start the new http request
debounceTime(2000),
map(_ => 'do something else')
)
.subscribe(console.log);
This code only print 'do something after debounce' after 5 seconds but never print 'domethingNow' after each key pressed
You can use the merge operator:
const searchBox = document.getElementById('search');
const keyup$ = fromEvent(searchBox, 'keyup');
const keyupEnd$ = keyup$.pipe(
switchMap(() => debounceTime(500))
);
const result = merge(
keyup$,
keyupEnd$
);
Have you tried something like:
import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';
var searchBox = document.getElementById('search');
var keyup$ = fromEvent(searchBox , 'keyup')
keyup$.pipe(
switchMap((i: any) => { console.log('do something');})
debounceTime(5000)) // with delay of 5 secs
.subscribe(console.log('do something else'););

rxjs switchMap need to return subscribed observable

Here here the requirement:
When click start button, emit event x times every 100ms, each emit correspond an UI update. When x times emit complete, it will trigger a final UI update, look simple right?
Here is my code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel$ = interval(100)
.pipe(
take(x),
share()
)
var startLight$ = start$
.pipe(
switchMap(() => {
intervel$
.pipe(last())
.subscribe(() => {
// Update UI
})
return intervel$
}),
share()
)
startLight$
.subscribe(function (e) {
//Update UI
})
Obviously, subscribe inside switchMap is anti-pattern, so I tried to refactor my code:
const startInterval$ = start$
.pipe(
switchMapTo(intervel$),
)
startInterval$.pipe(last())
.subscribe(() => {
//NEVER Receive value
})
const startLight$ = startInterval$.pipe(share())
The problem is that intervel$ stream is generated inside switchMap and can not be accessed outside, you can only access the stream who generate interval$, i.e. start$ which never complete!
Is there is smarter way to handle such kind of problem or it was an inherent limitation of rxjs?
You were very close. Use last() inside intervel$ to only emit the final one to the subscribe below. Working StackBlitz. Here are details from the StackBlitz:
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100)
.pipe(
tap(() => console.log('update UI')), // Update UI here
take(x),
last()
);
const startInterval$ = start$
.pipe( switchMapTo(intervel$));
startInterval$
.subscribe(() => {
console.log('will run once');
});
Update
If you do not wish to use tap(), then you can simply cause start$ to finish by taking only the first emission and then completing with either take(1) or first(). Here is a new StackBlitz showing this.
const start$ = fromEvent(document.getElementById('start'), 'click')
.pipe(
first()
);
const intervel$ = interval(100)
.pipe(
take(x)
);
const startInterval$ = start$
.pipe(
switchMapTo(intervel$)
);
startInterval$
.subscribe(
() => console.log('Update UI'),
err => console.log('Error ', err),
() => console.log('Run once at the end')
);
The downside to this approach (or any approach that completes the Observable) is that once completed it won't be reused. So for example, clicking multiple times on the button in the new StackBlitz won't work. Which approach to use (the first one that can be clicked over and over or the one that completes) depends on the results you need.
Yet Another Option
Create two intervel$ observables, one for the intermediate UI updates and one for the final one. Merge them together and only do the UI updating in the subscribe. StackBlitz for this option
code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel1$ = interval(100)
.pipe(
take(x)
);
const intervel2$ = interval(100)
.pipe(
take(x+1),
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
A more idiomatic way, same logic as previous one (By Guichi)
import { switchMapTo, tap, take, last, share, mapTo } from 'rxjs/operators';
import { fromEvent, interval, merge } from 'rxjs';
const x = 5;
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100);
const intervel1$ = intervel$
.pipe(
take(x)
);
const intervel2$ = intervel1$
.pipe(
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
Reflection
The key problem of the original question is to 'use the same observable in different ways', i.e. during the progress and the final. So merge is an pretty decent logic pattern to target this kind of problem
Put your update logic inside the switchMap and tap() , tap will run multiple time and only last emission will be taken by subscribe()
const startInterval$ = start$
.pipe(
switchMap(()=>intervel$.pipe(tap(()=>//update UI),last()),
)
startInterval$
.subscribe(() => {
// will run one time
})

Turn observable into subject

We have a function that gets a stream from the backend as observable. However we would like to be able to push to that observable as well to see the changes before those are done in the back-end. To do so I tried giving back a subject instead but the connection is still on going after the unsubscribe.
In other words, in the code below, we would like the console.log(i) not to start before we subscribe to the subject, and finish when we unsubscribe from it :
import { ReplaySubject, Observable, interval } from 'rxjs';
import { tap } from 'rxjs/operators'
function test() {
const obs = interval(1000).pipe(tap(i => console.log(i)));
const subj = new ReplaySubject(1);
obs.subscribe(subj);
return subj;
}
const subject = test();
subject.next('TEST');
const subscription = subject.pipe(
tap(i => console.log('from outside ' + i))
).subscribe()
setTimeout(_ => subscription.unsubscribe(), 5000);
example
You cannot subscribe in test. I guess you want to create an Observable and a Subject and merge those - you would have to return both separately.
return [subject, merge(subject, obs)]
and then
const [subject, obs] = test();
subject.next()
But I would do it by providing subject as a parameter.
import { ReplaySubject, Observable, interval, merge } from 'rxjs';
import { tap } from 'rxjs/operators'
function test(subject) {
return merge(
interval(1000).pipe(tap(i => console.log(i))),
subject
);
}
const subject = new ReplaySubject(1);
const obs = test(subject);
subject.next('TEST');
const subscription = obs.pipe(
tap(i => console.log('from outside ' + i))
).subscribe()
setTimeout(_ => subscription.unsubscribe(), 5000);

Resources