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)),
),
),
);
Related
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({}))
),
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)))
)
)
)
)
)
);
There has to be a better way to do this, right?
const source$ = combineLatest([
folder$.pipe(
filter(folder => folder.canBeLoaded()),
),
page$,
sort$,
]).pipe(
takeUntil(this.onceDestroyed$),
switchMap(([folder, page, sort]) => combineLatest([
of(folder),
of(page),
of(sort),
// buffer$: BehaviourSubjhect<number>
buffer$.pipe(
startWith(-1),
pairwise(),
filter(([buffered, wanted]) => wanted > buffered ),
map(([, current]) => current),
distinctUntilChanged(),
),
])),
);
withLatestFrom comes to my mind, but it will return once per parent, I need buffer$ to be able to emit multiple times!
no need to be that complicated, below should work upper/lower stream are always the latest.
const source$ = combineLatest([
folder$.pipe(
filter(folder => folder.canBeLoaded()),
),
page$,
sort$,
]).pipe(
takeUntil(this.onceDestroyed$),
switchMap(([folder, page, sort]) =>
buffer$.pipe(
startWith(-1),
pairwise(),
filter(([buffered, wanted]) => wanted > buffered ),
map(([, current]) => current),
distinctUntilChanged(),
map(current => [folder, page, sort,current]),
),
),
);
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!
In my store a have queue containing pending network requests.
When the action ATTEMPT_FLUSH is emmited, I want to sequentially send the requests.
However if one of them fails and emits ATTEMPT_FLUSH_CANCELLED, the next ones should not be attempted (until I try again in the next ATTEMPT_FLUSH, of course).
Here's what I have so far
export const attemptFlushEpic = (action$, store) =>
action$
.ofType(ATTEMPT_FLUSH)
.mergeMap(() => Observable.from(store.getState().queue)) // state.queue is an array
.concatMap(action =>
Observable.ajax(action.url)
.map(response => removeFromQueue(action))
.catch(err => Observable.of(attemptFlushCancelled())));
Moving up the actual ajax call should cancel the subsequent ajax on failure.
export const attemptFlushEpic = (action$, store) =>
action$
.ofType(ATTEMPT_FLUSH)
.mergeMap(() => Observable.from(store.getState().queue)
.concatMap(action => Observable.ajax(action.url)
.map(response => removeFromQueue(action)))
.catch(err => Observable.of(attemptFlushCancelled())))
);