I've a survey which contains list of questions, where each question contains list of multiple choice answer. I'm trying to push this to my backend, but it's pushing ONLY the first question of the survey according to my database. I made sure that the questions and the answers are inserted correctly and also i tested the mutation using Graphql
this._surveyService.addSurveyToInitiative(this._initiativeId, this.name, this.category, "TBD").subscribe(surveyData => {
this.survey.questions.forEach(question => {
this._surveyService.addQuestionToSurvey(surveyData.data.createSurvey.survey.id, question.question).subscribe(questionData=> {
question.multipleChoiceAnswers.forEach(answer => {
this._surveyService.addMultipleChoiceAnswerToQuestion(questionData.data.addQuestionToSurvey.newQuestion.id, answer).subscribe()
})
})
})
})
}
From the first blush, the problem doesn't appear to be RxJS or subscribe. Something about your service calls is likely the culprit. That being said, it's hard to debug.
Nesting subscribes and/or calling subscribe in a loop is basically always an antipattern. You're mixing imperative and functional programming styles with abandon - that's not to say you can't make it work but that teasing apart what is going wrong (not to mention maintaining/extending this in the future) becomes more challenging.
First lets rewrite this more functionally:
this._surveyService.addSurveyToInitiative(
this._initiativeId,
this.name,
this.category,
"TBD"
).pipe(
map(surveyData =>
this.survey.question.map(question =>
this._surveyService.addQuestionToSurvey(
surveyData.data.createSurvey.survey.id,
question.question
).pipe(
map(questionData => ({ question, questionData }))
)
)
),
mergeMap(serviceCalls => merge(...serviceCalls)),
map(({ question, questionData }) =>
question.multipleChoiceAnswers.map(answer =>
this._surveyService.addMultipleChoiceAnswerToQuestion(
questionData.data.addQuestionToSurvey.newQuestion.id,
answer
)
)
),
mergeMap(serviceCalls => merge(...serviceCalls))
).subscribe();
Here, this line: mergeMap(serviceCalls => merge(...serviceCalls)) (Seen twice) is effectively how you subscribe to all your calls.
This should be much easier to debug. Now you can tap any part of your stream to see what's happening at that moment.
this._surveyService.addSurveyToInitiative(
this._initiativeId,
this.name,
this.category,
"TBD"
).pipe(
tap(surveyData => console.log("Processing surveryData: ", surveyData)),
map(surveyData =>
this.survey.question.map(question =>
this._surveyService.addQuestionToSurvey(
surveyData.data.createSurvey.survey.id,
question.question
).pipe(
map(questionData => ({ question, questionData }))
)
)
),
mergeMap(serviceCalls => merge(...serviceCalls)),
tap(questionAndData => console.log("Processing questionAndData: ", questionAndData)),
map(({ question, questionData }) =>
question.multipleChoiceAnswers.map(answer =>
this._surveyService.addMultipleChoiceAnswerToQuestion(
questionData.data.addQuestionToSurvey.newQuestion.id,
answer
)
)
),
mergeMap(serviceCalls => merge(...serviceCalls))
).subscribe(x => console.log("done processing final step, got reponse: ", x));
Now you can delay certain calls, or use concat(... instead of merge(... to get a feel for what your stream is doing.
Aside:
Here is the same thing, only a bit more concise (We combine our merge calls):
this._surveyService.addSurveyToInitiative(
this._initiativeId,
this.name,
this.category,
"TBD"
).pipe(
mergeMap(surveyData =>
merge(...this.survey.question.map(question =>
this._surveyService.addQuestionToSurvey(
surveyData.data.createSurvey.survey.id,
question.question
).pipe(
map(questionData => ({ question, questionData }))
)
))
),
mergeMap(({ question, questionData }) =>
merge(...question.multipleChoiceAnswers.map(answer =>
this._surveyService.addMultipleChoiceAnswerToQuestion(
questionData.data.addQuestionToSurvey.newQuestion.id,
answer
)
))
)
).subscribe();
Update # 1
Are you sure
this.survey.question.map(question =>
is right? Seems like a strange naming convention. I would expect something more akin to
this.survey.questions.map(question =>
You should not chain subscriptions. Also forEach is a final operation, which means that nothing is returned from it. You should use map instead.
A better way would be to use mergeMap to get something like follow:
this._surveyService.addSurveyToInitiative(this._initiativeId, this.name, this.category, "TBD")
.pipe(
map(surveyData => surveyData.data.createSurvey.survey.id),
mergeMap(surveyId => this.survey.questions.map(question => this._surveyService.addQuestionToSurvey(surveyId, question.question))),
map(questionData => questionData.data.addQuestionToSurvey.newQuestion.id),
mergeMap(questionId => question.multipleChoiceAnswers.forEach(answer => this._surveyService.addMultipleChoiceAnswerToQuestion(questionId, answer)))
).subscribe();
Related
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([]))
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.
I'm quite new to RXJS and development in general. I started working with rxjs recently and I found myself stuck with the following issue and I would appreciate some help/ guidance and some explanation please.
export const updateSomethingEpic1 = (action$) =>
action$
.ofType('UPDATE_SOMETHING')
.switchMap(({ result }: { result }) =>
//SOME API CALL
.map(({ response }) => updateSomethingSuccess(response))
**make call to second epic**
.catch(err => updateSomethingError(err)),
);
//My second epic
export const updateSomethingEpic2 = (action$) =>
action$
.ofType('UPDATE_SOMETHING2')
.switchMap(({ result }: { result }) =>
//SOME API CALL
.map(({ response }) => updateSomethingSuccess2(response))
.catch(err => updateSomethingError2(err)),
);
My question is how would I make a call to my second epic after my first epic has called the api and made a successful request. Want to make a call in the first epic after updateSomethingSuccess action, which adds response to the store and then call the second api afterwards.
Just do return an action.
Code from your sample
.map(({ response }) => updateSomethingSuccess(response) **<--- make call to second epic**)
use
.map(({ response }) => updateSomethingSuccess(response) )
where updateSomethingSuccess(response) is action creator like following
function updateSomethingSuccess(response) {
return { type: 'UPDATE_SOMETHING2', payload: response}; // HERE
}
Then your epic 2 with ofType UPDATE_SOMETHING2 will be performed.
It's dead simple. You have to modify for your needs
I just started using Redux-Observable. I want to make a generic epic that requests data from the server. I want multiple requests with same action and id to be debounced. However, I'm not sure how to do this without creating multiple epics.
const TYPE_IFEXISTS_REQUEST = 'IFEXISTS_REQUEST';
export const IFEXISTS_REQUEST = (id, value) =>
({ type: TYPE_IFEXISTS_REQUEST, id, value });
export const IFEXISTS_EPIC = action$ =>
action$
.ofType(TYPE_IFEXISTS_REQUEST)
.debounceTime(5000) // ERROR: debounces based on action type and not id
.mergeMap(action =>
fromPromise(api.get(`/api/exists/${action.id}/${action.value}`))
.map(({ data }) => IFEXISTS_SUCCESS(action.id, data))
.catch(() => of(IFEXISTS_FAILURE(action.id, 'Server error'))));
How is it possible to create a generic epic that debounces based on both action and id?
Update: Never knew about GroupBy. It worked well with switchMap. The following is was I used.
action$
.ofType(TYPE_IFEXISTS_REQUEST)
.groupBy(action => action.id)
.mergeMap(actionByIdGroup$ =>
actionByIdGroup$
.debounceTime(5000) // debounces based on action id
.switchMap(action =>
fromPromise(api.get(`/api/exists/${action.id}/${action.value}`))
.map(({ data }) => IFEXISTS_SUCCESS(action.id, data))
.catch(() => of(IFEXISTS_FAILURE(action.id, 'Server error')))
);
)
You can use the groupBy operator:
action$
.ofType(TYPE_IFEXISTS_REQUEST)
.groupBy(action => action.id)
.mergeMap(actionByIdGroup$ =>
actionByIdGroup$
.debounceTime(5000) // debounces based on action id
.mergeMap(action =>
fromPromise(api.get(`/api/exists/${action.id}/${action.value}`))
.map(({ data }) => IFEXISTS_SUCCESS(action.id, data))
.catch(() => of(IFEXISTS_FAILURE(action.id, 'Server error')))
);
)
The actionByIdGroup$ is a Grouped Observable of the same action ids. Meaning, only actions with the same id will be part of the same stream. In this case, the debounceTime will be applied for actions with the same id.
In redux-observable I need to wait before doing an API request until another epic has completed. Using combineLatest doesn't work, nothing happens after APP_INITIALIZED finishes. I also tried withLatestFrom but neither works.
const apiGetRequestEpic = (action$, store) => {
return Observable.combineLatest(
action$.ofType('APP_INITIALIZED'),
action$.ofType('API_GET_REQUEST')
.mergeMap((action) => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => {
return {type: types.API_RESPONSE, payload: xhr.response};
})
.catch(error => {
return Observable.of({ type: 'API_ERROR', payload: error });
});
})
);
};
combineLatest definition
One approach is (using pseudo names), when receiving the initial action API_GET_REQUEST you immediately start listening for a single INITIALIZE_FULFILLED, which signals that the the initialization (or whatever) has completed--we'll actually kick it off in a bit. When received, we mergeMap (or switchMap whichever for your use case) that into our call to make the other ajax request and do the usual business. Finally, the trick to kick off the actual initialization we're waiting for is adding a startWith() at the end of that entire chain--which will emit and dispatch the action the other epic is waiting for.
const initializeEpic = action$ =>
action$.ofType('INITIALIZE')
.switchMap(() =>
someHowInitialize()
.map(() => ({
type: 'INITIALIZE_FULFILLED'
}))
);
const getRequestEpic = (action$, store) =>
action$.ofType('API_GET_REQUEST')
.switchMap(() =>
action$.ofType('INITIALIZE_FULFILLED')
.take(1) // don't listen forever! IMPORTANT!
.switchMap(() => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => ({
type: types.API_RESPONSE,
payload: xhr.response
}))
.catch(error => Observable.of({
type: 'API_ERROR',
payload: error
}));
})
.startWith({
type: 'INITIALIZE'
})
);
You didn't mention how everything works, so this is just pseudo code that you'll need to amend for your use case.
All that said, if you don't ever call that initialization except in this location, you could also just include that code directly in the single epic itself, or just make a helper function that abstracts it. Keeping them as separate epics usually means your UI code can independently trigger either of them--but it might still be good to separate them for testing purposes. Only you can make that call.
const getRequestEpic = (action$, store) =>
action$.ofType('API_GET_REQUEST')
.switchMap(() =>
someHowInitialize()
.mergeMap(() => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => ({
type: types.API_RESPONSE,
payload: xhr.response
}))
.catch(error => Observable.of({
type: 'API_ERROR',
payload: error
}));
})
.startWith({ // dunno if you still need this in your reducers?
type: 'INITIALIZE_FULFILLED'
})
);
I think that you are not using combineLatest the way it is intended. Your example should be written as:
const apiGetRequestEpic = (action$, store) => {
return (
Observable.combineLatest(
action$.ofType('APP_INITIALIZED').take(1),
action$.ofType('API_GET_REQUEST')
)
.map(([_, apiGetRequest]) => apiGetRequest)
.mergeMap((action) => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => {
return {type: types.API_RESPONSE, payload: xhr.response};
})
.catch(error => {
return Observable.of({ type: 'API_ERROR', payload: error });
});
})
);
};
This way, this combineLatest will emit whenever an 'API_GET_REQUEST' is emitted, provided that 'APP_INITIALIZED' has ever been dispatched at least once, or, if not, wait for it to be dispatched.
Notice the .take(1). Without it, your combined observable would emit anytime 'APP_INITIALIZED' is dispatched as well, most likely not what you want.