NGRX concat two action - rxjs

I am using NGRX and Angular 11.
I am trying to, when an action is executed, call another action, listen to the success of it, and if it succeeded dispatch a final action:
Here my code a bit simplified :
#Effect()
getUserSettingSuccess$ = this.actions$.pipe(
// When this action `GetUserSettingsSuccess` is executed
ofType<featureActions.GetUserSettingsSuccess>(featureActions.ActionTypes.GetUserSettingsSuccess),
concatMap((action) =>
of(action).pipe(withLatestFrom(this.store$.pipe(select(ProjectsStoreSelectors.selectedProjectId))))
),
// I want to dispatch a new action
tap(([action, projectId]) => new ModelsStoreAction.GetAllModelsRequest({ projectId })),
// and listen to the success / failure of that action.
// If success dispatch `SetModelSelection` else do nothing.
map(([action]) =>
this.actions$.pipe(
ofType(ModelsStoreAction.ActionTypes.GetAllModelsSuccess),
takeUntil(this.actions$.pipe(ofType(ModelsStoreAction.ActionTypes.GetAllCesiumModelsFailed))),
first(),
map(() => new ModelsStoreAction.SetModelSelection())
)
)
The problem I have is that, the above code do not dispatch a valid action. But I am getting a bit lost with all those rxjs operator.

Here would be my approach:
#Effect()
getUserSettingSuccess$ = this.actions$.pipe(
// When this action `GetUserSettingsSuccess` is executed
ofType<featureActions.GetUserSettingsSuccess>(featureActions.ActionTypes.GetUserSettingsSuccess),
concatMap((action) =>
of(action).pipe(withLatestFrom(this.store$.pipe(select(ProjectsStoreSelectors.selectedProjectId))))
),
// I want to dispatch a new action
tap(([action, projectId]) => new ModelsStoreAction.GetAllModelsRequest({ projectId })),
// and listen to the success / failure of that action.
// If success dispatch `SetModelSelection` else do nothing.
switchMap(
([action]) => this.actions$.pipe(
// adding all the possibilities here
ofType(ModelsStoreAction.ActionTypes.GetAllModelsSuccess, ModelsStoreAction.ActionTypes.GetAllCesiumModelsFailed),
first(),
filter(a => a.type === ModelsStoreAction.ActionTypes.GetAllModelsSuccess.type),
map(() => new ModelsStoreAction.SetModelSelection()),
)
)
)
It didn't work before because this.actions$ is essentially a type of Subject and an effect is not expected to return an Observable-like value. In order to solve that, I used switchMap, which will handle the inner subscription automatically.

Related

ngrx effect - catchError: returning of does not end stream, and the API call does not re-run

I have an effect in NgRX effect, as follows:
$createOrganisation = createEffect(() =>
this.actions$.pipe(
ofType(fromOrganisationActions.createOrganisation),
switchMap((data) => this.organisation.createOrganisation(data)),
map((response) => fromOrganisationActions.createOrganisationSuccess({ orgId: response.id })),
catchError((error) => {
return of(fromOrganisationActions.createOrganisationError(error));
})
)
);
However, my stream never seems to end when the catchError is triggered, i.e. in the instance of this.organisation.createOrganisation returning a 400 error.
The action, fromOrganisationActions.createOrganisationError(error) is triggered and my reducer is triggered from this... but if I re-trigger the fromOrganisationActions.createOrganisation effect, this effect runs but the API call is never made a second time.
If I configure it as follows, and dispatch it manually, it works:
$createOrganisation = createEffect(() =>
this.actions$.pipe(
ofType(fromOrganisationActions.createOrganisation),
switchMap((data) => this.organisation.createOrganisation(data)),
map((response) => fromOrganisationActions.createOrganisationSuccess({ orgId: response.id })),
catchError((error) => {
this.store.dispatch(fromOrganisationActions.createOrganisationError(error));
return throwError(error);
})
)
);
But other examples online suggest the first way should work, but it is not for me, and I do not understand why my stream never ends.
Can somebody please advise and tell me why my stream never ends in the first instance?
The catchError should be added to the inner observable.
$createOrganisation = createEffect(() =>
this.actions$.pipe(
ofType(fromOrganisationActions.createOrganisation),
switchMap((data) => this.organisation.createOrganisation(data).pipe (
map((response) =>
fromOrganisationActions.createOrganisationSuccess({ orgId: response.id })),
catchError((error) => {
return of(fromOrganisationActions.createOrganisationError(error));
})
)),
)
);
These mistakes can be caught with eslint-plugin-ngrx, more details about the rule here.

Emit before and after every retry

I have an epic, that listens for a certain action.
Once it gets the action, it should do a ajax.post
Branch
If status code is good, then emit YES
If status bad, then emit pre, wait 1s, emit post
I am struggling mightily with the last bullet, here is my code in a playground - https://rxviz.com/v/WJxGMl4O
Here is my pipeline part:
action$.pipe(
flatMap(action =>
defer(() => ajax.post('foo foo foo')).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
retryWhen(error$ =>
error$.pipe(
tap(error => console.log('got error:', error)),
merge(of('pre')), // this isnt emiting
delay(1000),
merge(of('post')) // this isnt emitting
)
)
)
)
)
I think you can achieve what you want by using catchError instead of retryWhen because retryWhen only reacts to next notifications but won't propagate them further. With catchError you get also the source Observable which you can return and thus re-subscribe. concat subscribes to all its source one after another only after the previous one completed so it'll first send the two messages pre and post and after that retry.
action$.pipe(
filter(action => action === 'hi'),
mergeMap(action =>
defer(() => resolveAfter(3)).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
catchError((error, source$) => {
console.log('retrying, got error:', error);
return staticConcat(
of('pre'),
of('post').pipe(delay(1000)),
source$,
);
}),
)
),
//take(4)
)
Your updated demo: https://rxviz.com/v/A8D7BzyJ
Here is my approach:
First, I created 2 custom operators, one that will handle 'pre' & 'post'(skipValidation) and one that will handle the logic(useValidation).
const skipValidation = src => of(src).pipe(
concatMap(
v => of('post').pipe(
startWith('pre'),
delay(1000),
),
),
);
What's important to notice in the snippet below is action$.next({ skip: true }). With that, we are emitting new values that will go through the iif operator so that we can emit 'pre' & 'post';
const useValidation = src => of(src).pipe(
filter(action => action === 'hi'),
mergeMap(action =>
defer(() => resolveAfter(3)).pipe(
tap(res => console.log('succeeded:', res)),
mapTo('YES'),
delay(1000),
retryWhen(error$ =>
error$.pipe(
tap(error => { console.log('retrying, got error:', error); action$.next({ skip: true })}),
delay(1000),
)
)
)
)
);
action$.pipe(
tap(v => console.log('v', v)), // Every emitted value will go through the `iif ` operator
mergeMap(v => iif(() => typeof v === 'object' && v.skip, skipValidation(v), useValidation(v))),
)
Here is your updated demo.

Add event listeners, and use `scan` to keep a reduced state, then remove event listeners

I am working on this sandbox here - https://stackblitz.com/edit/rxjs-g7msgv?file=index.ts
What I am trying to do is:
1) Wait for onLogin event
2) While logged in, I want to connectSocket(), and whenever the socket gets disconnected, and the app is in the foreground, I want to re-connectSocket(). (in the sandbox I have stubbed out connectSocket() to a promise that just waits 5 sec)
3) I want to repeat step 2, until onLogout event comes in
I wrote this code here, please see the sandbox and start things off by pressing the "onLogin" button.
fromEvent(document, 'onLogin')
.pipe(
switchMap(() =>
of({ isDisconnected: true, isInForeground: true }).pipe(
mergeMap(data =>
concat(
fromEvent(document, 'onDisconnect').pipe(
mergeMap(() =>
data.isDisconnected = true
)
),
fromEvent(document, 'onAppStateChange').pipe(
mergeMap(({ detail:{ state } }) =>
data.isInForeground = state === 'foreground'
)
),
).pipe(
mergeMap(({ isDisconnected, isInForeground }) => {
if (isDisconnected && isInForeground) {
return flatMap(() => connectSocket());
} else {
return EMPTY;
}
})
)
),
takeUntil(fromEvent(document, 'onLogout'))
)
)
)
.subscribe(console.log);
I use switchMap because while its running, I don't want any other login events to restart another flow.
I'm not able to get this working. I am new to rxjs.
Use startWith to Init the value and combineLatest will fire when either one of the event is triggered.
fromEvent(document, 'onLogin').pipe(
switchMap(() =>
combineLatest(
fromEvent(document, 'onDisconnect').pipe(
mapTo(true),
startWith(true)
),
fromEvent(document, 'onAppStateChange').pipe(
map(e => e.detail === 'foreground'),
startWith(true),
)
).pipe(
mergeMap(([isDisconnected, isInForeground]) =>
isDisconnected && isInForeground ? connectSocket() : EMPTY
),
takeUntil(fromEvent(document, 'onLogout'))
)
)
)

catch error and emit different action when handling ngrx-effects

In the code snippet below, I want to emit a GetData action if the update operation was successful or a BackendError action if unsuccessful.
#Effect()
updateData$ = this.actions$.pipe(
ofType(MyActionType.UPDATE_DATA),
map((action: UpdateData) => action.payload),
combineLatest(this.authService.getUser(), (myData, user) => this.dataService.updateData(myData, user)),
map(() => new GetData()),
catchError((err) => { of(new BackendError(err)))
);
The above code does not seem to be working. Even though the update operation fails due to permission error, the BackendError action is not emitted.
Any help is appreciated.
Your current implementation will swallow errors on the updateData operation, because the action$ (the outer observable) gets mapped to a new GetData() action, regardless of the result of the updateData operation (success or failure).
In the implementation below, the catchError operator will runif the updateData operation (dataUpdate$ -- the inner observable) throws an error..
#Effect()
updateData$ = this.actions$.pipe(
ofType(MyActionType.UPDATE_DATA),
mergeMap((action: UpdateData) => {
const user$ = this.authService.getUser();
const dataUpdate$ = user$.pipe(
mergeMap(user => this.dataService.updateData(action.payload, user));
);
return dataUpdate$.pipe(
map(() => new GetData()),
catchError(err => of(new BackendError(err)))
);
})
);
Additional Resources
Here's some more information on the difference between mergeMap
and map.
Official docs for mergeMap.
Example of #Effect using this approach in the #ngrx/effects README.

redux-observable epic that doesn't send any new actions

Might be that I'm a noob and not fully understanding how this stuff should work yet, but I have an epic in redux-observable in which I want to use as a way to create a promise which will dispatch an action and wait for a different action before resolving. I've got it working by mapping the action to '__IGNORE__' but I really don't want to do that. Is there any way to just have an epic handle an action, but not pass anything else on?
Here's my code:
export const waitFor = (type, action) => new Promise((resolve, reject) => {
const waitForResult = action$ => action$.ofType(type).do(() => resolve()).mapTo({type: "___IGNORE___"});
registerEpic(waitForResult);
action();
});
You can throw away any next'd values from an observable chain by using the .ignoreElements() RxJS operator
action$.ofType(type)
.do(() => resolve())
.ignoreElements();
Another way of doing this (no more right or wrong) is to create an anonymous Observable that just subscribes.
const waitForResultEpic = action$ => new Observable(observer =>
action$.ofType(type)
.subscribe(() => resolve())
);
This is implicitly returning the subscription we create, so that it's attached to the lifecycle of our rootEpic as well. Because we never call observer.next(), this epic never emits any values; just like ignoreElements().
Although you didn't ask, you may eventually notice that your epic will run forever, listening for that incoming action matching the type variable. This may not be not what you want, if you want to match once then complete.
You can accomplish that using .take(1) operator.
const waitForResult = action$ =>
action$.ofType(type)
.take(1)
.do(() => resolve())
.ignoreElements();
Or
const waitForResult = action$ => new Observable(observer =>
action$.ofType(type)
.take(1)
.subscribe({
next: () => resolve(),
error: err => observer.error(err),
complete: () => observer.complete()
})
);
This will only match once per the life of the application--once received it will never do it again.

Resources