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({}))
),
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!
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!
I got duplicate action firing when I subscribe to a socket io event.
const onStartGameActionEpic = (action$, state$) =>
action$.pipe(
ofType(ON_START_GAME),
mergeMap(() =>
fromEvent(socket, 'newGameCreated').pipe(
map(response => onStartGameFulfilled(response)),
),
),
);
You're creating a new listener every time ON_START_GAMEoccurs, but you're never killing off the old ones. That's the root of the issue.
Fix 1
Change mergeMap to switchMap.
Fix 2
You might want to have multiple games going at once. If so, assign a namespace prop to your ON_START_GAME action. When the game end action fires, kill off that specific observable.
const onStartGameActionEpic = (action$, state$) =>
action$.pipe(
ofType(ON_START_GAME),
mergeMap(onStartGameAction =>
fromEvent(socket, 'newGameCreated').pipe(
takeUntil(action$.pipe(
ofType(ON_END_GAME),
filter(onEndGameAction => (
endGameAction.namespace === startGameAction.namespace
)),
)),
map(response => onStartGameFulfilled(response)),
),
),
);