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.
Related
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();
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
Context
I have a lot of actions of the form: LOAD_XYZ,LOAD_XYZ_FAIL, LOAD_XYZ_SUCCESS for different page elements that need to be loaded.
I frequently wish to perform a redirect (react-router) after some item gets loaded, but since redux-observable does not return a promise I cannot perform the redirect in my component - but instead I have to do it using redux and push('...') from react-router-redux.
That unfortunately leads to a lot of duplication as I have LOAD_XYZ and LOAD_XYZ_REDIRECT_TO versions of the same action, plus two epics for each.
This is starting to feel wasteful and with too much redundancy
const fetchUserEpic = action$ =>
action$.ofType(FETCH_USER)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.map(response => fetchUserFulfilled(response))
);
const fetchUserAndRedirectEpic = action$ =>
action$.ofType(FETCH_USER_AND_REDIRECT)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.concatMap(response => [fetchUserFulfilled(response), push(action.redirectTo)])
);
Problem
Is there some sort of pattern/approach where I can sequence actions to be dispatched after some kind of async request has completed, to avoid redundancy and having to implement multiple versions of the same epic.
For example, I would like instead to have a separate REDIRECT_TO action that I can sequence after the item was loaded.
Something like these (imaginary) solutions:
dispatch(fetchUser(...)).then(redirectTo("..."))
dispatchSequence(fetchUser(...), redirectTo("..."))
I know redux-thunk can do this but then I miss out on all the rxjs operators.
I'm probably trenching into dangerous grounds but I did find a way to do this.
First we make a middleware which will add a completion observable as metadata to actions.
import { Subject } from "rxjs";
export const AddListenerMiddleware = store => next => action => {
const forkedAction = Object.assign({}, action, { meta: { listener: new Subject()}});
next(forkedAction);
return forkedAction.meta.listener.asObservable();
};
Since this is a Subject we can call next on it to emit some value. Creating a notify helper allows us to notify any subscribers of when the action has completed (plus pass down the data).
// notify doesn't consume but just emits on the completion observable
const notify = (action, fn) => source =>
Observable.create(subscriber =>
source.subscribe(emmission => {
if (action.meta && action.meta.listener) {
action.meta.listener.next(fn(emmission));
}
subscriber.next(emmission);
})
);
const fetchUserEpic = action$ =>
action$.ofType(FETCH_USER)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.map(response => fetchUserFulfilled(response))
.pipe(notify(action, response => response.data))
);
And now our dispatch returns an observable on which we can do standard rxjs operations. So this finally this becomes possible:
dispatch(fetchUser(...))
.subscribe(({id}) => dispatch(redirectTo("/" + id)))
Disclaimer: Use at your own risk!
I'm using react-observable to develop a react-native application.
I have two epics, one is used to fetch the latitude and longitude, and another is used to get the address through the latitude and longitude.
My question is, as I want to do one thing in one epic, how could I use the first epic's output as the second epic's input?
const getLocationEpic = action$ =>
action$.ofType('GET_LOCATION')
.mergeMap(() =>
currentPosition$()
.map(data => ({
type: 'GET_LOCATION_SUCCESS',
data: {"latitude": data.coords.latitude, "longitude": data.coords.longitude}
}))
.catch(error => {
console.log("GET POSITION ERR", error);
return Observable.of({type: 'GET_POSITION_ERROR', error: error});
})
);
const getAddressEpic = action$ =>
action$.ofType('GET_ADDRESS')
.switchMap(action =>
ajax(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}`)
.map(data => (data))
.do({
next: data => {
console.log("address data");
console.log(data);
}
})
.map(data => ({
type: 'GET_POSITION_SUCCESS', data: data.response.results[0].formatted_address
}))
.mergeMap(() =>
currentPosition$()
.map(data => ({
type: 'GET_ADDRESS_SUCCESS',
data: {"latitude": data.coords.latitude, "longitude": data.coords.longitude}
}))
.catch(error => {
console.log("GET_ADDRESS_ERROR", error);
return Observable.of({type: 'GET_ADDRESS_ERROR', error: error});
})
));
it might be something like this:
getLocationEpic(action)
.mergeMap( (action) => getAddressEpic(action) )
.subscribe( (data) => {
/* get the result from epic 1 to epic 2 */
})
Detail: Take result of getLocationEpic, then flatten it and feed the result into getAddressEpic. Subscribe on it for the final result.
In redux-observable your epics receive a stream of all actions dispatched--what may not be obvious is that includes actions dispatched by other epics. This is a very powerful feature because you can write epics that don't need to be coupled to implementation details in the other epics. The action is the contract.
In your case, you said you want to sequence them as 1. get longitude/latitude 2. then use that long/lat to get their address.
Your getAddressEpic then just needs to listen for GET_LOCATION_SUCCESS to be notified and receive the long/lat values.
const getAddressEpic = action$ =>
action$.ofType('GET_LOCATION_SUCCESS') // <--- listen for location success action
.switchMap(action =>
// you can now use the long/lat from that action
ajax(`${apiURL}/geocode/json?latlng=${action.data.latitude},${action.data.longitude}`)
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.