Any reason to use shareReplay(1) in a BehaviorSubject pipe? - rxjs

I'm using a library that exposes data from a service class using a pretty common BehaviorSubject pattern. The only notable difference with the implementation and what I have seen/used myself is the addition of a pipe with a shareReplay(1) operator. I'm not sure if the shareReplay is required. What effect, if any, does the shareReplay have in this case?
// "rxjs": "^6.3.0"
this.data = new BehaviorSubject({});
this.data$ = this.data.asObservable().pipe(
shareReplay(1)
)
Note: I've read a number of articles on shareReplay, and I've seen questions about different combinations of shareReplay and Subject, but not this particular one

Not in your example but imagine if there was some complex logic in a map function that transformed the data then the share replay would save that complex logic being run for each subscription.
const { BehaviorSubject } = rxjs;
const { map, shareReplay } = rxjs.operators;
const bs$ = new BehaviorSubject('initial value');
const obs$ = bs$.pipe(
map(val => {
console.log('mapping');
return 'mapped value';
}),
shareReplay({bufferSize:1, refCount: true})
);
obs$.subscribe(val => { console.log(val); });
obs$.subscribe(val => { console.log(val); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.1/rxjs.umd.min.js"></script>
Compare without the share, the map happens twice.
const { BehaviorSubject } = rxjs;
const { map } = rxjs.operators;
const bs$ = new BehaviorSubject('initial value');
const obs$ = bs$.pipe(
map(val => {
console.log('mapping');
return 'mapped value';
})
);
obs$.subscribe(val => { console.log(val); });
obs$.subscribe(val => { console.log(val); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.1/rxjs.umd.min.js"></script>

With this pattern (use of shareReplay(1)), the service protects itself from the user using the next() function of the BehaviorSubject while sending the last value of the BehaviorSubject (which would not have been the case without shareReplay(1)).

Related

Why is a Promise changing behavior of getValue in RXJS?

I had an issue where adding an extra pipe to subscription to a BehaviorSubject was making the wrong behavior in some tests. Whenever I did const stores = await lastValueFrom(workingStore$); in RXJS 7 or const stores = await workingStore$.toPromise(); in RXJS 6, the value was not what I expected. I reduced the code down to this fiddle: https://jsfiddle.net/Dave_Stein/v7aj6bwy/
You can see on the run without a concatMap, getValue gives 3 values in an array. With concatMap, it will only return the first value.
The same can be observed when I use toPromise in this way:
console.log('here i am', bug);
const promise = workingStore$.toPromise();
from(events).subscribe(events$);
const x = await promise;
console.log('there i am', bug, x);
I get that there is an async behavior going on with concatMap, but I would imagine using toPromise would make RXJS wait for all the events being processed via subscribe to complete before resolving the promise.
In reality my concatMap calls a method that is async and MUST use await based on a library I am using.
Is there some other way to accomplish this? Order of events matters to me which is why I chose concatMap
The solution is at: https://jsfiddle.net/Dave_Stein/nt6Lvc07/.
Rather than trying to subscribe to workingStore$ twice, I can use mergeWith operator in RXJS 7. (There is another way to accomplish this in 6). Using subscribe on the same subject twice is a bad practice that can lead to issues like these apparently.
const { Subject, operators, pipe, BehaviorSubject, from, lastValueFrom } = rxjs;
const { filter, scan, concatMap, mergeWith } = operators;
const run = async (bug) => {
const events$ = new Subject();
const historyChanges$ = new Subject();
const workingStore$ = new BehaviorSubject({});
const scanWorkingStoreMap = {};
events$.pipe(
concatMap((evt => {
return Promise.resolve(evt);
})),
filter((evt) => evt.name === 'history')
).subscribe(historyChanges$);
const newDocs$ = events$
.pipe(
filter((evt) => evt.name === 'new'));
// historyChanges$ is a Subject
historyChanges$
.pipe(
mergeWith(newDocs$),
scan((acc, evt) => {
const { id } = evt;
if (!acc[id]) {
acc[id] = [evt]
} else {
acc[id].push(evt)
}
return acc;
}, scanWorkingStoreMap),
)
.subscribe(workingStore$);
const events = [
{ name: 'new', id: 1},
{ name: 'history', id: 1, data: { a: 1}},
{ name: 'history', id: 1, data: { a: 2}}
]
console.log('here i am', bug);
from(events).subscribe(events$);
console.log('there i am', await lastValueFrom(workingStore$));
}
run();

How to do onething after another in rxjs

I find myself wanting to do this, which feels like it ought to be wrong.
this.isLoading = true;
this.service.getFirstValue().subscribe((response: firstValueType) => {
this.firstValue = response;
this.service.getSecondValue(this.firstValue).subscribe((response: secondValueType) => {
this.secondValue = response;
this.isLoading = false
});
});
What are you supposed to do?
you can use switchMap. Also embedded subscribe is bad practice, supposed to be avoided
this.isLoading = true;
this.service
.getFirstValue()
.pipe(
switchMap(response => {
this.firstValue = response;
return this.service.getSecondValue(this.firstValue);
})
)
.subscribe(response => {
this.secondValue = response;
this.isLoading = false;
});
PS fix the code, subscribe should be outside of pipe
The solution I figured out works fine for me in my project was using the following parts of rxjs:
ConnectableObservable
switchMap
tap
const { Subject } = rxjs;
const { publishReplay, tap, switchMapTo } = rxjs.operators;
// Simulates your service.getFirstValue, ...
const source1$$ = new Subject();
const source2$$ = new Subject();
const source3$$ = new Subject();
// Publish and Replay the last value to be able to connect (make it hot) and always get the lates value
const source1$ = source1$$.pipe(publishReplay(1));
const source2$ = source2$$.pipe(publishReplay(1));
const source3$ = source3$$.pipe(publishReplay(1));
// Connect to make the observable hot
source1$.connect()
source2$.connect()
source3$.connect()
source1$.pipe(
tap(v => console.log('tap source1$: ', v)),
switchMapTo(source2$),
tap(v => console.log('tap source2$: ', v)),
switchMapTo(source3$),
tap(v => console.log('tap source3$: ', v))
).subscribe()
source3$$.next('value 1');
source2$$.next('value 2');
source1$$.next('value 3');
source2$$.next('value 4');
source2$$.next('value 5');
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
I am not sure if this fits your exactly requirements. But imo this is a at least close szenario like yours. If you want give me feedback and I try to adapt :)
Nested subscribe should be avoided.
In this case I would go like this
this.isLoading = true;
this.service.getFirstValue().pipe(
// this tap is necessary only if you use this.firstValue somewhere else in the code
// otherwise you can skip it and go directly to concatMap
tap((response_1: firstValueType) => this.firstValue = response_1),
// concatMap is usually the preferred operator to use when concatenating http calls
// read the article I have linked below
concatMap(response_1 => this.service.getSecondValue(response_1)),
tap(response_2 => {
this.secondValue = response_2;
this.isLoading = false;
})
).subscribe()
You can get some more inspiration on how to deal with http calls and rxJs reading this article.

Multiple subscriptions nested into one subscription

I find myself puzzled trying to set a very simple rxjs flow of subscriptions. Having multiple non-related subscriptions nested into another.
I'm in an angular application and I need a subject to be filled with next before doing other subscriptions.
Here would be the nested version of what I want to achieve.
subject0.subscribe(a => {
this.a = a;
subject1.subscribe(x => {
// Do some stuff that require this.a to exists
});
subject2.subscribe(y => {
// Do some stuff that require this.a to exists
});
});
I know that nested subscriptions are not good practice, I tried using flatMap or concatMap but didn't really get how to realize this.
It's always a good idea to separate the data streams per Observable so you can easily combine them later on.
const first$ = this.http.get('one').pipe(
shareReplay(1)
)
The shareReplay is used to make the Observable hot so it won't call http.get('one') per each subscription.
const second$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('second', firstCallResult))
);
const third$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('third', firstCallResult))
);
After this you can perform subscriptions to the Observables you need:
second$.subscribe(()=>{}) // in this case two requests will be sent - the first one (if there were no subscribes before) and the second one
third$.subscribe(() => {}) // only one request is sent - the first$ already has the response cached
If you do not want to store the first$'s value anywhere, simply transform this to:
this.http.get('one').pipe(
flatMap(firstCallResult => combineLatest([
this.http.post('two', firstCallResult),
this.http.post('three', firstCallResult)
])
).subscribe(([secondCallResult, thirdCallResult]) => {})
Also you can use BehaviorSubject that stores the value in it:
const behaviorSubject = new BehaviorSubject<string>(null); // using BehaviorSubject does not require you to subscribe to it (because it's a hot Observable)
const first$ = behaviorSubject.pipe(
filter(Boolean), // to avoid emitting null at the beginning
flatMap(subjectValue => this.http.get('one?' + subjectValue))
)
const second$ = first$.pipe(
flatMap(firstRes => this.http.post('two', firstRes))
)
const third$ = first$.pipe(
flatMap(()=>{...})
)
behaviorSubject.next('1') // second$ and third$ will emit new values
behaviorSubject.next('2') // second$ and third$ will emit the updated values again
You can do that using the concat operator.
const first = of('first').pipe(tap((value) => { /* doSomething */ }));
const second = of('second').pipe(tap((value) => { /* doSomething */ }));
const third = of('third').pipe(tap((value) => { /* doSomething */ }));
concat(first, second, third).subscribe();
This way, everything is chained and executed in the same order as defined.
EDIT
const first = of('first').pipe(tap(value => {
// doSomething
combineLatest(second, third).subscribe();
}));
const second = of('second').pipe(tap(value => { /* doSomething */ }));
const third = of('third').pipe(tap(value => { /* doSomething */ }));
first.subscribe();
This way, second and third are running asynchronously as soon as first emits.
You could do something like this:
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(() => subject1),
tap(x => {
// Do some stuff that require this.a to exists
}),
switchMap(() => subject2),
tap(y => {
// Do some stuff that require this.a to exists
})
);
if you want to trigger this, simply call this.subject$.next();
EDIT:
Here is an possible approach with forkJoin, that shout call the subjects parallel.
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(
() => forkJoin(
subject1,
subject2
)),
tap([x,y] => {
// Do some stuff that require this.a to exists
})
);

Do something if RxJs subject's refCount drops to zero

I'm working on a service layer that manages subscriptions.
I provide subject-backed observables to consumers like this:
const subject = new Subject();
_trackedSubjects.push(subject);
return subject.asObservable();
Different consumers may monitor the channel, so there may be several observables attached to each subject.
I'd like to monitor the count of subject.observers and if it ever drops back to 0, do some cleanup in my library.
I have looked at refCount, but this only is available on Observable.
I'd love to find something like:
subject.onObserverCountChange((cur, prev) =>
if(cur === 0 && prev !== 0) { cleanUp(subject) }
)
Is there a way to automatic cleanup like this on a subject?
Instead of using Subject - you should probably describe setup/cleanup logic when creating observable. See the example:
const { Observable } = rxjs; // = require("rxjs")
const { share } = rxjs.operators; // = require("rxjs/operators")
const eventSource$ = Observable.create(o => {
console.log('setup');
let i = 0
const interval = setInterval(
() => o.next(i++),
1000
);
return () => {
console.log('cleanup');
clearInterval(interval);
}
});
const events$ = eventSource$.pipe(share());
const first = events$.subscribe(e => console.log('first: ', e));
const second = events$.subscribe(e => console.log('second: ', e));
setTimeout(() => first.unsubscribe(), 3000);
setTimeout(() => second.unsubscribe(), 5000);
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>

rxjs, How can I merge multiple subjects in to one Observable, but process them with different method

I have three subject. like this:
const s1$ = new Subject()
const s2$ = new Subject()
const s3$ = new Subject()
these three subjects call next() emit same value: const fruit = {id: 1, name: apple};
and, I have three methods to handle the logic one to one correspondence of the subjects call next(fruit) method.
method1() {
//called when s1$.next(fruit)
}
method2() {
//called when s2$.next(fruit)
}
method3() {
//called when s3$.next(fruit)
}
I want to implement this:
// here maybe not Observable.merge, it's just a thinking.
Observable.merge(
s1$,
s2$,
s3$
)
.doSomeOperator()
.subscribe(val => {
//val could be s1$ emit, s2$ emit or s3$ emit
//but the val is same, the fruit.
//do some map like s1->method1, s2->method2, s3->method3, so I can omit if...else statement.
const method = this.method1 | this.method2 | this.method3.
method();
})
How can I implement this, thanks.
Use map operator to add a distinguish sources.
export class AppComponent {
s1(val) {
console.log('s1', val);
}
s2(val) {
console.log('s2', val);
}
constructor() {
const s1= new Subject();
const s2= new Subject();
const m1= s1.map(val=> ({val, source:'s1'}));
const m2 = s2.map(val=> ({val, source:'s2'}));
Observable.merge(m1, m2)
.subscribe(({val, source}) => {
this[source](val);
});
s1.next('apple');
s2.next('apple');
}
}
If there are no priority order (given that no matter which Subject emits, you want to have the method called), I would suggest the following:
// here maybe not Observable.merge, it's just a thinking.
Observable.merge(
s1$.map((val) => ({fn: (arg) => this.method1(arg), val})),
s2$.map((val) => ({fn: (arg) => this.method2(arg), val})),
s3$.map((val) => ({fn: (arg) => this.method3(arg), val}))
)
.subscribe({fn, val}=> {
fn(arg)
});
You can also execute them at the map operator. But well, it depends what you are trying to achieve here

Resources