The following code works without errors:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() => concat(
// fires an actionCreator and triggers another epic
of(actionOne()),
// fires an actionCreator
of(actionTwo())
))
);
The problem is that I need the data from actionOne to be available before actionTwo gets fired, and it doesn't seem to be happening. So I want to make this an async function like:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(async () => concat(
of(await actionOne()),
of(actionTwo())
))
);
This throws an error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
EDIT
Other relevant code:
// main component that loads
constructor(props) {
props.dispatch(init());
}
componentDidUpdate(prevProps) {
if (prevProps.actionTwoFlag !== this.props.actionTwoFlag) {
// do stuff with result from actionOne
// error is thrown here because there's no data
}
}
// actions
export const init = () => ({ type: "TEST" });
export const actionOne = () => ({ type: "ACTION_ONE" });
export const actionOneDone = (result) => ({ type: "ACTION_ONE_DONE", payload: result });
export const actionTwo = () => ({ type: "ACTION_TWO", payload: true });
// epics
export const actionOneEpic = (action$: any) => action$.pipe(
ofType("ACTION_ONE"),
mergeMap(() =>
ajax(..).pipe(
mergeMap(result => concat(
of(actionOneDone(result)),
...
))
)
)
);
);
There are various ways to solve this.
1- One way is just using defer() operator on the actionTwo. What defer() operator would do, is execute your code on subscription, since they are concatenated, the subscription to of(actionTwo()) would be done after of(actionOne()) is completed:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() => concat(
of(actionOne()),
defer(() => of(actionTwo()))
))
);
2- Another option is just do a switchMap(), this would ensure too that when you create the of(actionTwo()) observable, the of(actionOne()) observable has already been emitted and finished. switchMap() also ensures sequential order, so you can safely remove the concat() operator:
export const myEpic = (action$: any) => action$.pipe(
ofType("TEST"),
mergeMap(() =>
of(actionOne()).pipe(switchMap(() => of(actionTwo())))
)
);
EDIT:
Now I think I got it, although, I am not pretty familiar with redux observable epics. I have seen a solution here: Composing and sequencing multiple epics in redux-observable
that may solve your issue two. Based on that, I will give 2 proposals.
1st proposal:
This proposal just builds an epic that push action one at first, and waits for action one done in order to push the action two.
export const myEpic = (action$: any) => action$.pipe(
ofType('TEST'),
map(() => actionOne()),
mergeMap(() => {
return action$.pipe(
ofType('ACTION_ONE_DONE'),
take(1),
map(() => actionTwo()),
);
})
);
2nd proposal:
Do it all in one epic. Since both action one and action two are related (one depend on each other) it could make sense to merge both into only one epic, it would be something like this:
export const myEpic = (action$: any) => action$.pipe(
ofType('TEST'),
map(() => actionOne()),
mergeMap(() => {
return ajax(..).pipe(
mergeMap((data) => {
return concat(
actionOneDone(action),
of(actionTwo()).mergeMap(() => /* Do action two */ actionTwoDone())
)
}),
)
})
);
Hope this helps!
Related
I need to sequentially call multiple HTTP requests. Currently I have this huge epic where I pass all API requests and then try to fire them one by one using mergeMaps. It works, but I believe there must be some easier or cleaner way but. All I found for RxJS was forkJoin but it fires everything in parallel. Am I nitpicking here or is there some much smarter way? Any tips could be helpful here.
Pseudo-code:
All requests are similar and look like this:
import { ajax } from "rxjs/ajax";
import { map } from "rxjs/operators";
export const unpublishAbcApi = (id) =>
ajax({
method: "PATCH",
url: ...,
headers: ..,
}).pipe(
map(({ response }) => ({
published: response.published,
}))
);
And this is my epic
export const deleteEverything = (action$, _,) =>
action$.pipe(
actionOfType(delete.request),
mergeMap(({ payload: { id }}) =>
unpublishAbcApi(id).pipe(
mergeMap(() =>
deleteDefApi(id).pipe(
mergeMap(() =>
deleteGhiApi(otherId).pipe(
mergeMap(() =>
deleteJklApi(id).pipe(
map(() => ({ id }))
)
)
)
)
)
)
);
),
mergeMap(({ id }) =>
// ...get the id and dispatchSuccess etc.
),
catchError(error => /* error handling */)
);
Since the apis are not interdependent on each other, you can just use concat which will execute APIs in sequence!
export const deleteEverything = (action$, _) =>
action$.pipe(
actionOfType(delete.request),
switchMap(({ payload: { id }}) => concat(
unpublishAbcApi(id),
deleteDefApi(id),
deleteGhiApi(otherId),
deleteJklApi(id)
).pipe(map() => ({id}))),
mergeMap(({ id }) =>
// ...get the id and dispatchSuccess etc.
),
catchError(error => /* error handling */)
);
In short, I would like to apply a switchMap to observables depending on their payload.
I currently have an epic with redux-observable that performs GETs on /api/document/{name}. I include name in the payload of my actions. The challenge is that I only filter based on the action type, and so I don't know how to filter dynamically based on the action's payload too.
const documentFetchEpic: Epic<TRootAction, TRootAction, TRootState> = (action$, store) =>
action$.pipe(
filter(isActionOf(documentActions.fetch)),
// TODO: Should filter and then switchMap based on different documentName
mergeMap(action =>
merge(
ApiUtils.documents.fetch(action.payload).pipe(
mergeMap(response => [
documentActions.fetchSuccess({
name: action.payload,
data: response.data,
}),
]),
catchError(err => of(documentActions.fetchFailure(err)))
)
)
)
);
If I made this mergeMap a switchMap, it would apply a switchMap independent of the action value. Instead, I would just like to use a switchMap if action.payload is the same.
Discovered the groupBy command and came up with this... does this seem reasonable?
const documentFetchEpic: Epic<TRootAction, TRootAction, TRootState> = (action$, store) =>
action$.pipe(
filter(isActionOf(documentActions.fetch)),
groupBy(({ payload }) => payload),
mergeMap(group =>
group.pipe(
switchMap(action =>
merge(
ApiUtils.documents.fetch(action.payload).pipe(
mergeMap(response => [
documentActions.fetchSuccess({
name: action.payload,
data: response.data,
}),
]),
catchError(err => of(documentActions.fetchFailure(err)))
)
)
)
)
)
);
I am a back-end developer starting with front-end development for a project I am working on. The front-end uses Angular7 and NgRx. I have studied a lot in the last 4 days, but here is something I am stuck with and would appreciate your help.
I learnt that we can dispatch multiple actions from an effect in NgRx by returning an Observable array having multiple actions. I want to dispatch one of the action in the array based on a condition.
My code looks something like this
#Effect()
something$: Observable<Action> = this.actions$.pipe(
ofType(ActionType),
switchMap.(action: any) => {
return service.call(action.payload)
.pipe(
switchMap((data: ReturnType) => [
new Action1(),
new Action2(),
]),
catchError(error handling)
);
}),
);
and I want to achieve something like this
#Effect()
something$: Observable<Action> = this.actions$.pipe(
ofType(ActionType),
switchMap.(action: any) => {
return service.call(action.payload)
.pipe(
switchMap((data: ReturnType) => [
if(condition)
new Action1()
else
new Action1.1() ,
new Action2(),
]),
catchError(error handling)
);
}),
);
I think its my lack of knowledge of RxJs, which is preventing me to implement the condition.
You can dispatch multiple actions or specific actions by letting conditional ifs determine what iterable to return
I recommend you read: https://www.learnrxjs.io/operators/transformation/switchmap.html
#Effect()
something$: Observable<Action> = this.actions$.pipe(
ofType(ActionType),
switchMap(action: any) => {
return service.call(action.payload)
.pipe(
switchMap((data: ReturnType) => {
let actionsToDispatch = [];
if(condition) {
actionsToDispatch.push(new SomeAction())
} else {
actionsToDispatch.push(new SomeOtherAction())
}
return actionsToDispatch
}),
catchError(error handling)
);
}),
);
To dispatch multiple actions you can pass the action array as shown below:
#Effect()
getTodos$ = this.actions$.ofType(todoActions.LOAD_TODOS).pipe(
switchMap(() => {
return this.todoService
.getTodos()
.pipe(
switchMap(todos => [
new todoActions.LoadTodosSuccess(todos),
new todoActions.ShowAnimation()
]),
catchError(error => of(new todoActions.LoadTodosFail(error)))
);
})
);
To dispatch actions conditionally you can wrap the actions in if/else as shown below:
#Effect()
getTodos$ = this.actions$.ofType(todoActions.LOAD_TODOS).pipe(
switchMap(() => {
return this.todoService
.getTodos()
.pipe(
switchMap(todos => {
if(true) {
return new todoActions.LoadTodosSuccess(todos),
} else {
return new todoActions.ShowAnimation()
}),
catchError(error => of(new todoActions.LoadTodosFail(error)))
);
})
);
Hope that helps!
I have a redux-observable epic which polls an API and I'm trying to execute three dependent, sequential http requests and gather all responses in an array.
toArray() is never executed in this case because concatMap() is not finished. I tried to move the calls inside of a mergeMap() and gather the array there, but only the last call was in the array.
timer(0, POLLING_INTERVAL).pipe(
concatMap(() => from(fetchApi(url1))),
concatMap(response => {
const url2 = 'URL based on first response';
return from(fetchApi(url2));
}),
concatMap(response => {
const url3 = 'URL based on second response';
return from(fetchApi(url3));
}),
toArray(), // expected [{response1}, {response2}, {response3}]
map(data => ({
type: ActionTypes.FETCH_SUCCESS,
payload: { data },
})),
catchError(error =>
of({
type: ActionTypes.FETCH_FAILED,
payload: { error },
}),
),
takeUntil(
action$.pipe(
ofType(ActionTypes.CANCEL_POLLING),
),
),
);
This depends on what you want to do. toArray() won't help you because timer never completes and toArray() emits only when its source completes.
Maybe you're looking for something like this:
timer(0, POLLING_INTERVAL).pipe(
concatMap(() => from(fetchApi(url1)).pipe(
concatMap(response1 => {
const url2 = 'URL based on first response';
return forkJoin([of(response1), fetchApi(url2)]);
}),
concatMap(([response1, response2]) => {
const url3 = 'URL based on second response';
return forkJoin([of(response1), of(response2), fetchApi(url3)]);
}),
)),
map(data => ({
type: ActionTypes.FETCH_SUCCESS,
payload: { data },
})),
...
I am trying to get my NgRX effect to return an action object. The action object indicates a "success", and needs to return all retrieved institutions as part of the payload. How do I do this? Please note that the block inside of the mergeMap does not work. I need to return all the retrieved institutions as part of a payload after retrieving them.
#Effect()
getInstitutions$ = this.actions$
.ofType(institutionActions.InstitutionActionTypes.GetInstitutions)
.pipe(
switchMap(() => {
const institutions = this.getInstitutions();
console.log(institutions);
return this.getInstitutions();
}),
mergeMap((institutions: Institution[]) => {
// return institutions;
return {
type: institutionActions.InstitutionActionTypes.GetInstitutionsSuccess,
payload: institutions
};
})
);
You have to operate on this.getInstitutions() as in:
#Effect()
getInstitutions$ = this.actions$
.ofType(institutionActions.InstitutionActionTypes.GetInstitutions)
.pipe(
switchMap(() => {
return this.getInstitutions().pipe(
map(institutions => ({
type: institutionActions.InstitutionActionTypes.GetInstitutionsSuccess,
payload: institutions
})),
catchError(_ => ...) //don't forget to handle errors
)
})
);
As a side note, I would encourage you to use action creators, instead of creating action objects "all over the place" - Let’s have a chat about Actions and Action Creators within NgRx
How about using "map" instaed mergeMap. Can you try following?
#Effect()
getInstitutions$ = this.actions$
.ofType(institutionActions.InstitutionActionTypes.GetInstitutions)
.pipe(
switchMap(() => {
const institutions = this.getInstitutions();
console.log(institutions);
return this.getInstitutions();
}),
map((institutions: Institution[]) => {
// return institutions;
return {
type: institutionActions.InstitutionActionTypes.GetInstitutionsSuccess,
payload: institutions
};
})
);
Hope that helps!