How to chain multiple .map() calls on previous items using rxjs - javascript

In the example below I was wondering how you would go about preforming two operations on the same response from the .swichMap().
In the example I put the second .map in which is clearly wrong but sort of illiterates what I want to do. How would I go about calling two functions. Also when I break the fist map() out into a function like .map(response => {fn1; fn2;}); typescript throws an error?
#Effect()
getUserCourse$: Observable<Action> = this.actions$
.ofType(userCourse.ActionTypes.LOAD_USER_COURSE)
.map<string>(action => action.payload)
.switchMap(userCourseId => this.userCourseApi.getUserCourse(userCourseId))
.map(response => new userCourse.LoadUserCourseSuccessAction(response.data));
.map(response => new course.LoadCourseSuccessAction(response.course));

For this answer I'm assuming that both functions userCourse.LoadUserCourseSuccessAction and course.LoadCourseSuccessAction do return Observables. If not you can always create one with Rx.Observable.of or Rx.Observable.fromPromise in case of for example an AJAX call.
If I understand you correctly you want to do independent things with the response, but do them in parallel and merge the results back in the stream. Have a look at the following code that shows how this can be archived.
Rx.Observable.of(
{data: 'Some data', course: 'course1'},
{data: 'Some more data', course: 'course2'}
).mergeMap((obj) => {
// These two streams are examples for async streams that require
// some time to complete. They can be replaced by an async AJAX
// call to the backend.
const data$ = Rx.Observable.timer(1000).map(() => obj.data);
const course$ = Rx.Observable.timer(2000).map(() => obj.course);
// This Observable emits a value as soon as both other Observables
// have their value which is in this example after 2 seconds.
return Rx.Observable.combineLatest(data$, course$, (data, course) => {
// Combine the data and add an additinal `merged` property for
// demo purposes.
return { data, course, merged: true };
});
})
.subscribe(x => console.log(x));
Runnable demo

Related

Don't cancel the first observable if the second fails, how to keep it (forkJoin, zip). Angular 11

I have parallel API calls. How to continue getting data from one of them if second one failed?
forkJoin([a,b])
.subscribe({
next: ((data) => {
const [first, second] = data;
})
It seems to me that combineLatest is what you are looking for, with a catchError to each of your observables to manage errors. Something like:
combineLatest([
obs1$.pipe(catchError(() => of(null)),
obs2$.pipe(catchError(() => of(null)),
]).subscribe(([first, second]) => { console.log(first,second); });
I have already found a solution, we need to handle errors in each observable and return of().
catchError(() => of([]))

RxJs concatMap is emitting only one http stream out of many

I have been trying to get stream of objects in a sequencial order, however concatMap is not working for me. With mergeMap I get the stream but no in a sequencial order.
Below is my code I am trying:
this.result
.pipe(
map((result: Result) => {
return result.contents
.map((content: Content) => this.databaseService
.getResource<Resource>('Type', content.key) // http call in service
)
}),
mergeMap((requests: Observable<Resource>[]) => {
return from(requests)
.pipe(
concatMap((resource: Observable<Resource>) => resource), // ----> Trigger only once.
filter((resource:Resource) => resource.status === 'active'),
skip(this.pageIndex * this.PAGE_SIZE),
take(this.PAGE_SIZE),
)
}),
)
.subscribe({
next: (resource: Resource) => {
console.log(resource) // resource stream
},
complete: () => console.log('completed')
});
concatMap will only process one observable source at a time. It will not process subsequent sources until the current source completes.
In your case the observable from this.databaseService.getResource() is not completing. To force it to complete after the first emission, you could append a take(1) like this:
concatMap((resource: Observable<Resource>) => resource.pipe(take(1))
Alternatively, if the call to databaseService.getResource() is only meant to emit a single value, you could modify your service to emit only a single value.
// http call in service
Note: If you are using the Angular HttpClient, a typical get request will complete when the response is received. So you can probably have your getResource() method return the oservable from the http.get() call.

Update array state from out of a for each loop

I'm trying to update the state of an array from out of a forEach loop without loosing the previous state. I`m trying to archive something like the following:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
let currentState = request;
request[idx] = Object.assign(...request[idx], {answer: response.answer});
setRequests(currentState);
})
})
}
But in such a case only one response will be rendered. Any idea how to archive something like this?
The Problems
There are a couple of issues there:
You're breaking one of the fundamental rules of React state: Do not modify state directly. Doing const currentState = request; doesn't copy the object, it just makes two variables point to the same object.
You're using the version of state setter where you just pass in the update, which completely overwrites any previous outstanding state updates.
Solutions
There are (at least) two approaches here:
Do piecemeal updates as you are in that code, where earlier updates are stored in state (causing a re-render) while later updates are still in progress
or
Get all the updated information, and do a single state update (and render)
Both are valid depending on your use case.
Piecemeal Updates
For the piecemeal updates, since you're doing a bunch of state updates (which may not occur in order, depending on the vagaries of the timing of the fetch replies). Since you're updating state based on previous state (the other entries in the array), you need to use the callback version of the state setter. Then, you need to create a new array each time, and and new object within the array for the object at index idx, like this:
// *** Note I've renamed `request` to `requests` -- it's an array, it should
// use the plural, and you use `request` for an individual one later.
// I've also reversed `response` and `data`, since they were backward in the
// original. I've also added (minimal) handling of errors.
const [requests, setRequests] = useState(initialState);
const run = () => {
requests.forEach((request, idx) => {
// (It seems odd that nothing from `request` is used in the `fetch`)
fetch("/ask")
.then((response) => response.json())
.then((data) => {
setRequests((prevRequests) =>
prevRequests.map((request, index) => {
return idx === index ? { ...request, answer: data.answer } : request;
})
);
})
.catch((error) => {
// ...handle/report error...
});
});
};
map creates the new array, returning a new object for the one at index idx, or the previous unchanged ones for the others.
All At Once
The other approach is to do all the fetch calls, wait for them all to complete, and then do a single state update, like this:
// (Same naming and error-handling updates)
const run = () => {
Promise.all(requests.map((request) =>
fetch("/ask")
.then((response) => response.json())
.then((data) => {
return {...request, answer: data.answer};
})
))
.then(setRequests)
.catch((error) => {
// ...handle/report error...
});
};
You can build the new array at once and update it, something like this
const run = () => {
Promise.all(request.map((r) => fetch("/ask").then(data => data.json())))
.then((responses) => {
const newRequest = responses.map(res => ({answer: res.answer}))
setRequests(newRequest);
})
}
Using Promise.all allows you to retrieve all the json response at once, so you can build the new state in one shot.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
note: code not tested.
You problem stems from the fact that let currentState = request will keep a reference to the request at the time of making the closure. This means that it won't get updated by future calls.
You need to use the functional version of setState. Something like this:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
setRequests(currentState => {
currentState[request[idx]] = {answer: response.answer};
return {...currentState};
});
})
})
}

Managing promises in RXJS observables

I've poked about SO and found many similar questions/answers but I may be missing something in my basic understanding on how to work with with this stack.
I'm working on a react native project along with RXJS/obervables. At some point I doing file downloads, this part is not a problem. A combo of pre-existing axios-rxjs and react-native-file-system get me where I want. The issue is I'm not sure how to handle it cleanly without async/await which I understand is an anti-pattern.
I want to transform this working code into a clean obervable-style flow.
I've implemented an Epic that does the operations I want as such:
const myDownloadEpic = (
action$,
state$
) =>
action$.pipe(
ofType(myDownloadActionType), // catches the relevant action
map(action => action.payload),
mergeMap(payload =>
downloadManager // a class with the relevant utils to get files
.download(payload), // this axios call returns my Blob file as Observable<Blob>
.pipe(
mergeMap(async response => {
// a promise is returned by RNFS to read the contents of a folder
const files = await RNFS.readDir(RELEVANT_TARGET_PATH)
...
// a promise returned from a custom function that converts my blob to bas64
const data = await convertToBase64(response)
// another promise returned by RNFS to write to the file system
await RNFS.writeFile(FULL_PATH, data, 'base64');
...
})
)
)
)
I've tried splitting this into several pipes, for example, I tried splitting the READ operation into a previous pipe but it ends up looking very verbose. Is there not a clean simple way to "hold" until the promises are done so I can make decisions based on their result?
What would be considered cleaner in this situation?
You can try something like this. It should be roughly equivalent to what you've written above.
const myDownloadEpic = (
action$,
state$
) => action$.pipe(
ofType(myDownloadActionType),
map(action => action.payload),
mergeMap(payload => downloadManager.download(payload)),
mergeMap(response => from(RNFS.readDir(RELEVANT_TARGET_PATH)).pipe(
map(files => ({response, files}))
)),
mergeMap(values => from(convertToBase64(values.response)).pipe(
map(data => ({...values, data}))
)),
mergeMap(({response, files, data}) => RNFS.writeFile(FULL_PATH, data, 'base64'))
);
The from() operator can convert a promise into an observable that will emit the promised value then completes.
If you need to wait until all promises are resolved, I recommend forkJoin() as it won't emit a value until all observables complete.
Lastly, to make the code a little cleaner, I would also recommend declaring separate variables/functions to define your observables for each promise.
const files$ = from(RNFS.readDir(RELEVANT_TARGET_PATH));
const getData = (response: unknown) => from(convertToBase64(response)).pipe(
mergeMap(data=>
from(RNFS.writeFile(FULL_PATH, data, 'base64')).pipe(
mapTo(data)
)
)
);
const myDownloadEpic = (action$, state$) =>
action$.pipe(
ofType(myDownloadActionType),
map(({ payload }) => payload),
mergeMap(payload => downloadManager.download(payload)),
mergeMap(response =>
forkJoin({
files: files$,
data: getData(response)
})
),
map(({ files, data }) => {
// check something with `files` and `data`
})
);
I'm assuming RNFS.writeFile() response is void, so I put it as an effect when subscribing to getData(). The mapTo() operator ignores any emitted value from the source observable, and returns whatever value you put in the parameter.

Passing callback from action into Epic

I am currently using an action creator to pass a callback function into an Epic, however I run into the common error: Actions must be plain JavaScript objects...
Is there a way of executing the callback and still proceeding with the observable inside an Epic? I've also tried using the curly braces and an explicit return to assign the chain to a variable and return it after invoking the callback, but I run into the same problem.
Code:
const selectItem = (item, activeRow, cb) => ({ type: SELECT_ITEM, activeRow, item, cb });
const selectItemEpic = action$ =>
action$.ofType(SELECT_ITEM)
.mergeMap(action =>
Observable.forkJoin(
ajax.getJSON(...),
ajax.getJSON(...)
)
.map(res => returnSelectedItem({ ...res[0].response, ...res[1].response }, action.activeRow))
.map(() => action.cb()) // failing here
.takeUntil(action$.ofType(SELECT_ITEM))
);
The map operator is meant as a 1:1 mapping--so it expects you to return something in your projection function. Right now you're returning what ever the result of action.cb() is. If this isn't an action (e.g. it returns nothing/undefined), that would be the source of your error.
It's not 100% clear what returnSelectedItems() returns, if this actually returns the action you want to emit from your epic, you could instead use the do operator to make the action.cb() side effect without affecting the actual values flowing through the stream.
const selectItemEpic = action$ =>
action$.ofType(SELECT_ITEM)
.mergeMap(action =>
Observable.forkJoin(
ajax.getJSON(...),
ajax.getJSON(...)
)
.map(res => returnSelectedItem({ ...res[0].response, ...res[1].response }, action.activeRow))
.do(() => action.cb()) // do, instead of map
.takeUntil(action$.ofType(SELECT_ITEM))
);

Categories