I want to make one HTTP call, and then based on its results possibly replace it with another one. This is Angular.
this.http.get('foo.txt')
.map(response => response.text())
.if(text => text === "TRYAGAIN",
this.http.get('bar.txt').map(response => response.text()))
.subscribe(text => console.log("got text", text);
But although there is an if operator, it does not seem to do what I want here.
I considered (mis)using errors to do this:
this.http.get('foo.txt')
.map(response => response.text())
.map(text => {
if (text === "TRYAGAIN") throw "";
return text;
})
.catch(err => this.http.get('bar.txt').map(response => response.text()))
.subscribe(text => console.log("got text", text);
But this seems less than ideal as well. What is the correct idiom for handling this kind of situation?
You should use mergeMap (rxjs5) or flatMap (rxjs)
this.http.get('foo.txt')
.map(response => response.text())
.mergeMap(text => (text === 'TRYAGAIN' ?
this.http.get('bar.txt').map(response => response.text())) :
Observable.of(text))
.subscribe(...)
Related
I have a scenario like this,
this.uiDataObservable.pipe(
tap(() => this.showLoader.next(true)),
map((uiData) => this.createHttpRequest(uiData),
switchMap((httpRequest) => this.apiService.get(httpRequest),
map((response) => this.createUISucessData(response),
)
).subscribe(
(success) => /* show data on UI */,
(error) => /* show error on UI */,
);
Now in this example what is the right place to call this.showLoader.next(false).
I cannot use finalize because my stream never ends.
I cannot use the third callback complete of subscribe block as it never
gets calls on error.
What is the correct rxjs way of handling this situation?
Adding it to success and error callback is something not I am looking for here. I am expecting something more rxjs way. - OP
I'm not sure why using RxJS callbacks should be considered less RxJS than anything else. Regardless, in an effort to not repeat this.showLoader.next(false), you could create a custom operator that handles the calls for you.
That might look as follows:
function tapNxtErr<T>(fn: () => void): MonoTypeOperatorFunction<T> {
return tap({
next: _ => fn(),
error: _ => fn()
});
}
this.uiDataObservable.pipe(
tap(() => this.showLoader.next(true)),
map(uiData => this.createHttpRequest(uiData)),
switchMap(httpRequest => this.apiService.get(httpRequest)),
map(response => this.createUISucessData(response))
tapNxtErr(() => this.showLoader.next(false))
).subscribe({
next: success => /* show data on UI */,
error: error => /* show error on UI */
});
Found better alternative, code goes like this:
this.uiDataObservable.pipe(
tap(() => this.showLoader.next(true)),
map((uiData) => this.createHttpRequest(uiData),
switchMap((httpRequest) => {
return this.apiService.get(httpRequest).pipe(
catchError((error) => {
/* show error on UI */
return of({ /* return empty response or undefined */ })
})
)
},
map((response) => this.createUISucessData(response),
)
).subscribe(
(data) => {
this.showLoader.next(false);
/* show data on UI */,
});
I have the following effect
addItem$ = createEffect(() =>
this.actions$.pipe(
ofType(SkusActions.addItemRequest),
concatMap(({ payload, redirectTo }) =>
this.dataService.addItem(payload).pipe(
map(({ data }) =>
data
? SkusActions.addItemSuccess(payload))
: SkusActions.addItemFailure({})
),
catchError((error) => of(SkusActions.addItemFailure(error)))
)
)
)
);
Since I am using graphql, I have to make sure that the query is succeeded or failed, (the query can go trought but have error in graphql so no data field)
Now, In the case of everything went trought correctly, I would like to add a timer of 3second BEFORE the addItemSuccess fire.
I tried
addItem$ = createEffect(() =>
this.actions$.pipe(
ofType(SkusActions.addItemRequest),
concatMap(({ payload, redirectTo }) =>
this.dataService.addItem(payload).pipe(
map(({ data }) =>
data
? timer(3000).pipe(mapTo(SkusActions.addItemSuccess(payload)))
: SkusActions.addItemFailure({})
),
catchError((error) => of(SkusActions.addItemFailure(error)))
)
)
)
);
but it say the type do not match.
SkusActions.addItemFailure({}) returns an action (javascript object) while timer(3000).pipe(...) returns an Observable.
So you should use switchMap instead (or concatMap or mergeMap would work as well) and always return an Observable:
switchMap(({ data }) => data
? timer(3000).pipe(mapTo(SkusActions.addItemSuccess(payload)))
: of(SkusActions.addItemFailure({}))
),
I have the following observable implementation
public getFromAddressSubjectObservable() {
let _address: string;
return this.fromAddressSubject.pipe(
tap((address: string) => _address = address),
mergeMap(() => this.rpc.call('', 'smsglocalkeys', [])),
map((keys: SmsgLocalKeysResult) => [...keys.smsg_keys, ...keys.wallet_keys]),
filter((keys: Array<SmsgLocalKey>) => !keys.some(k => k.address === _address)),
mergeMap(() => this.rpc.call('', 'smsggetpubkey', [_address])),
mergeMap((pubkeyData: PublicKeyData) => this.rpc.call('', 'smsgaddaddress', [ pubkeyData.address, pubkeyData.publickey ])),
mergeMap(() => this.rpc.call('', 'smsgaddlocaladdress', [_address]))
)
}
I would like to know if there's a way that I could have this function without side effects, i.e. passing the value of _address from the first operator to the last one of the observable.
One way could be the following. You start defining a method, closureAddressMethod, which expects address as parameter, like this
public closureAddressMethod(address: string) {
return this.rpc.call('', 'smsglocalkeys', [])).pipe(
map((keys: SmsgLocalKeysResult) => [...keys.smsg_keys, ...keys.wallet_keys]),
filter((keys: Array<SmsgLocalKey>) => !keys.some(k => k.address === _address)),
mergeMap(() => this.rpc.call('', 'smsggetpubkey', [_address])),
mergeMap((pubkeyData: PublicKeyData) => this.rpc.call('', 'smsgaddaddress', [ pubkeyData.address, pubkeyData.publickey ])),
mergeMap(() => this.rpc.call('', 'smsgaddlocaladdress', [_address]))
)
}
and then you use this method within the pipe of getFromAddressSubjectObservable method, like this
public getFromAddressSubjectObservable() {
return this.fromAddressSubject.pipe(
mergeMap(address => closureAddressMethod(address))
)
}
Last point, unrelated to your question, is about using mergeMap in a situation which I see as a chain of sequential calls to some remote server. Maybe, in such cases, you may want to consider using concatMap, as suggested in this video from Ben Lesh.
I've written the Effect below to make requests for each item in the array, and when one fails, the error isn't handled and the observable stream completes resulting actions not triggering effects any further.
Effect:
#Effect() myEffect$ = this.actions$.pipe(
ofType<myAction>(myActionTypes.myAction),
mergeMapTo(this.store.select(getAnArray)),
exhaustMap((request: any[]) => {
return zip(...request.map(item => {
return this.myService.myFunction(item).pipe(
map(response => {
return this.store.dispatch(new MyActionSuccess(response))
}),
catchError(error => {
return Observable.of(new MyActionFailure(error));
})
)
}))
})
How do I handle the error in this case?
I wrote a generic request method that is supposed to show a loading indicator while the request is running. If an error occurs (like 404), I display the message in .catch and then return Observable.empty(), so that the following code doesn't crash (since no data is returned).
The big problem is that then .finally won't be called either. Is that a bug? Is there a workaround? Here's my code:
res = Observable
.of(true)
.do(() => this.store.dispatch(new ShowLoadingIndicatorAction()))
.switchMap(() => this.http.get(url, { headers: this.headers }))
.publishReplay(1)
.refCount()
.catch(error => {
this.messages.handleRequestError(error);
return Observable.empty();
})
.finally(() => this.store.dispatch(new HideLoadingIndicatorAction()));
// then later:
res.subscribe(doStuffWithData);
What RxJS-version are you using? It does works fine in this example:
const res$ = Rx.Observable
.of(true)
.switchMap(() => Rx.Observable.throw("Rest Error"))
.publishReplay(1)
.refCount()
.catch(error => Rx.Observable.empty())
.finally(() => console.log("Calling Finally!"));
res$.subscribe(console.info);
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>