I have an unexpected behavior with the operator withLatestFrom.
Output
map a
a
map a <== why a is mapped again ?
map b
b
const { Subject, operators } = window.rxjs
const { map, withLatestFrom } = operators
const createA = new Subject()
const createB = new Subject()
const a = createA.pipe(
map(() => console.log('map a'))
)
const b = createB.pipe(
withLatestFrom(a),
map(() => console.log('map b'))
)
a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })
createA.next()
createB.next()
<script src="https://unpkg.com/#reactivex/rxjs#6.6.3/dist/global/rxjs.umd.js"></script>
I found that the operator share() allows multiple subscribers.
const a = createA.pipe(
map(() => console.log('map a')),
share()
)
The problem here isn't with withLatestFrom() but rather with how subscriptions work. Observables are lazy and don't run until you subscribe. Each new subscriptions will run the observable again.
const stream$ = from([1,2,3]);
stream$.subscribe(console.log) // output: 1 2 3
stream$.subscribe(console.log) // output: 1 2 3
In this case, `from([1,2,3)] ran twice. If I alter my stream, anything I do will happen for each subscriber.
const stream$ = from([1,2,3]).pipe(
tap(_ => console.log("hi"))
);
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3
stream$.subscribe(console.log) // output: hi 1 hi 2 hi 3
The final piece of the puzzle is this: internally withLatestFrom() subscribes to the stream that you give it. Just like an explicit .subscribe() runs the observable, so does withLatestFrom() once it's subscribed to.
You can use shareReplay to cache the latest values and replay them instead of running the observable again. It's one way to manage a multicasted stream:
const createA = new Subject()
const createB = new Subject()
const a = createA.pipe(
tap(() => console.log('tap a')),
shareReplay(1)
)
const b = createB.pipe(
withLatestFrom(a),
tap(() => console.log('tap b'))
)
a.subscribe(() => { console.log('a') })
b.subscribe(() => { console.log('b') })
createA.next()
createB.next()
Now a.subscribe() and withLatestFrom(a) are both getting a buffered value that only gets run when createA.next() is executed.
As an aside, mapping a value to nothing is bad habit to get into. Consider the following code:
from([1,2,3]).pipe(
map(val => console.log(val))
).subscribe(val => console.log(val));
This will output
1
undefined
2
undefined
3
undefined
because you're actually mapping each value to nothing. tap on the other hand doesn't change the source observable, so it's a much better tool for debugging and/or side effects that don't alter the stream
from([1,2,3]).pipe(
tap(val => console.log(val))
).subscribe(val => console.log(val));
This will output
1
1
2
2
3
3
Related
How to pause observable every 15 seconds then wait for 5s and then continue emitting?
I made this example:
const digits = interval(1000);
const pauser$ = interval(20000).pipe(mapTo(true));
pauser$.subscribe(item =>
console.log(`${new Date().toISOString()} pause fired`)
);
const pauseStopper$ = pauser$.pipe(
concatMap(_ => timer(5000)),
tap(() => console.log(`${new Date().toISOString()} pause stopped`)),
mapTo(false)
);
const observable = merge(pauser$, pauseStopper$).pipe(
startWith(false),
switchMap(paused => (paused ? NEVER : digits))
);
observable.subscribe(
item => console.log(`${new Date().toISOString()}: ${item}`),
console.error,
() => console.log("complete")
);
After first pause cycle it behaves nearly close to what I need. Any ideas on how to make this more clear?
stackblitz example
I created a player function that gets three params:
source$: Your source observable that should be paused/played
play$: Continues to emit your source$ observable
pause$: Pauses your source$ observable
const player = <T>(source$: Observable<T>, play$: Observable<void>, pause$: Observable<void>): Observable<T> =>
merge(
play$.pipe(
switchMap(() => of(source$))
),
pause$.pipe(
switchMap(() => of(NEVER))
)
).pipe(
switchMap(stream => stream)
)
This function currently throws an error as far as I can see. In the short time I have atm I could not fix the error. The error is caused by of(void 0) in the pause$ switchMap. Later I will try to fix this error. Forfeit the error the function works.
You can now use a play$ and pause$ Observable to emit the play/pause:
const play$: Observable<void> = interval(20000).pipe(
startWith(void 0),
mapTo(void 0)
);
const pause$: Observable<void> = play$.pipe(
switchMap(() => interval(15000).pipe(
take(1)
)),
mapTo(void 0)
);
const player$ = player(source$, play$, pause$);
Does this solve your problem? If there are any issues with the solution let me know and I try to adapt.
I have 2 observables that both indicate if they're loading data or not. They come from #ngrx/data.
loadingA$: Observable<boolean>
loadingB$: Observable<boolean>
I'd like to "logical OR" combine the two to do whatever when either is true, using rxjs or more elegant method. Maybe ramdajs?, maybe a combined loading state? However different components need different combinations of loading streams.
Also, what if I have 20 streams, it shouldn't be limited to 2 streams only.
(!) I do not want to assign additional local variables.
combineLatest(loadingA$, loadingB$).pipe(map((a, b) => a || b));
or
const anyLoading = (...observables: Observable<boolean>[]) => combineLatest(observables).pipe(
map(bools => bools.some(loading => loading))
);
and use it
anyLoading(loadingA$, loadingB$);
const { combineLatest, BehaviorSubject } = rxjs;
const { map } = rxjs.operators;
const anyLoading = (...observables) => combineLatest(observables).pipe(
map(bools => bools.some(loading => loading))
);
const loadingA$ = new BehaviorSubject(false);
const loadingB$ = new BehaviorSubject(true);
anyLoading(loadingA$, loadingB$).subscribe(loading => { console.log(loading); });
loadingB$.next(false);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
I can't seem to be able to turn this cold observable into a hot one:
const test = new BehaviorSubject('test').pipe(tap(() => console.log('I want this to be logged only once to the console!')))
const grr = test.pipe(
share(), // share() seems to not do anything
take(1), // The culprit is here, causes logging to take place 5 times (5 subscribers)
share() // share() seems to not do anything
)
grr.subscribe(() => console.log(1))
grr.subscribe(() => console.log(2))
grr.subscribe(() => console.log(3))
grr.subscribe(() => console.log(4))
grr.subscribe(() => console.log(5))
// Expected output:
// 'I want this to be logged only once to the console!'
// 1
// 2
// 3
// 4
// 5
How should I change this to produce the wanted output?
You can use publishReplay and refCount operators like this:
import { interval, BehaviorSubject } from 'rxjs';
import { publishReplay, tap, refCount } from 'rxjs/operators';
const test = new BehaviorSubject('test').
pipe(
tap(() => console.log('I want this to be logged only once to the console!')
),
publishReplay(1),
refCount()
);
test.subscribe(() => console.log(1));
test.subscribe(() => console.log(2));
test.subscribe(() => console.log(3));
test.subscribe(() => console.log(4));
Working Stackblitz: https://stackblitz.com/edit/typescript-cvcmq6?file=index.ts
I'm trying to make multiple http requests and get returned data in one object.
const pagesToFetch = [2,3]
const request$ = forkJoin(
from(pagesToFetch)
.pipe(
mergeMap(page => this.mockRemoteData(page)),
)
)
mockRemoteData() return a simple Promise.
After first Observable emits (the once created from first entry of pagesToFetch the request$ is completed, second value in not included. How can I fix this?
You can turn each value in pagesToFetch into an Observable and then wait until all of them complete:
const observables = pagesToFetch.map(page => this.mockRemoteData(page));
forkJoin(observables)
.subscribe(...);
Or in case it's not that simple and you need pagesToFetch to be an Observable to collect urls first you could use for example this:
from(pagesToFetch)
.pipe(
toArray(),
mergeMap(pages => {
const observables = pages.map(page => this.mockRemoteData(page));
return forkJoin(observables);
}),
)
.subscribe(...);
Try the below sample format...
Observable.forkJoin(
URL 1,
URL 2
).subscribe((responses) => {
console.log(responses[0]);
console.log(responses[1]);
},
error => {console.log(error)}
);
I have the following searchService.search method that returns a forkJoin of two api calls.
I want the calls to execute simultaneously which they are but I also want each response back as a single object that can be passed into my SearchSuccess action and processed immediately without waiting for all calls to complete. Currently they are returning as an array of responses and only upon completion of both API calls - as this is what forkJoin is used for.
My issue is that I'm struggling to find another operator that does what I want.
Or perhaps the code pattern requires some redesign?
action:
#Effect()
trySearch: Observable<Action> = this.actions$.pipe(
ofType(SearchActionTypes.TrySearch),
switchMap((action: TrySearch) =>
this.searchService.search(action.payload)
.pipe(
map((data) => new SearchSuccess(data)),
catchError(error => of(new SearchFail(error))),
),
),
);
SearchService (snippet):
search(searchForm: SearchForm): Observable<any> {
const returnArray = [];
if (searchForm.searchClients) {
const searchClientParams = new Search();
searchClientParams.searchPhrase = searchForm.searchPhrase;
searchClientParams.type = SearchType.Client;
const searchClients = this.objectSearch(searchClientParams);
returnArray.push(searchClients);
}
if (searchForm.searchContacts) {
const searchContactParams = new Search();
searchContactParams.searchPhrase = searchForm.searchPhrase;
searchContactParams.type = SearchType.Contact;
const searchContacts = this.objectSearch(searchContactParams);
returnArray.push(searchContacts);
}
return Observable.forkJoin(returnArray);
}
If I understand it correctly returnArray contains two Observables and you want to wait until they both complete but still you want to emit each result separately.
Since forkJoin emits all results in a array you could just unwrap it with mergeMap (or concatMap):
this.searchService.search(action.payload)
.pipe(
mergeMap(results => results),
map((data) => new SearchSuccess(data)),
catchError(error => of(new SearchFail(error))),
),