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

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.

Related

NGRX concat two action

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.

RxJs: finalize for not complete observable

I have the following code:
this.userService.get().pipe(
catchError(() => EMPTY) // Do something here
).subscribe(() => {}) // And here
I want to do some stuff no matter there was an error or not. Just like finalize but for not completed observable. I tried to use tap, but it works only if there's no error. I don't want to duplicate my code and add it to the catchError. What should I do?
there's no other way to do it with catchError without touching it in the current example . Because everything after catchError won't get any notification, it's like an observable that never emits.
you can use 2nd argument of tap, it is triggered on error, the same as in subscribe.
this.userService.get().pipe(
tap(
() => console.log('emit'),
() => console.log('error'),
() => console.log('complete'),
),
// catchError(() => EMPTY), // closing the stream and hiding the error.
// repeat(), // in case if you want to resubscribe once the stream has been closed.
).subscribe(
() => console.log('emit'),
() => console.log('error'),
() => console.log('complete'),
);
Emit a default value in catchError.
this.userService.get().pipe(
catchError(() => of(null)) // catch error and emit null instead (or some other value)
).subscribe(value => {
// do something here,
// value will be null when this.userService.get() errors */
})
You should consider moving your error handling with catchError into the Service.

Possible to determine when epics finish in Redux Observable?

I'm new to RxJS so sorry if it doesn't make much sense.
Let's say I want to have a reusable epic for fetching a user that will be invoked by action from an app load epic.
Oversimplified example:
const getUserEpic = action$ =>
action$.pipe(
ofType(GET_USER_REQUEST),
switchMap(action => from(service.fetchUser(action.userId).pipe(
mapTo({ type: GET_USER_SUCCESS })))
),
);
const appLoadEpic = action$ =>
action$.pipe(
ofType(LOAD_APP_REQUEST),
map(() => of({ type: GET_USER_REQUEST }, { type: SOME_OTHER_REQUEST }))
);
What if I wanted to call LOAD_APP_SUCCESS after all of the invoked epics (getUser etc.) finish? It'd be great if it could be done in the appLoadEpic but I'm afraid it's not possible.
The way I would suggest doing it is combining the the individual epics into a "meta"-epic. That is, you can use the individual streams to listen to their individual events and the propagate them when all the merged streams are completed.
const getUserEpic = action$ => ...
const someOtherEpic = action$ => ...
// Creates an epic that merges all the results from each provided epic
const initializationEpic = combineEpics(getUserEpic, someOtherEpic)
const appLoadEpic = (action$, state$) => {
// Runs the new epic with the action and state values.
const onLoad$ = initializationEpic(action$, state$).pipe(
endWith({type: LOAD_APP_SUCCESS})
)
// Listen for the load app request before subscribing to the initialization
action$.pipe(
ofType(LOAD_APP_REQUEST),
mergeMapTo(onLoad$),
)
}
If you are feeling fancy and don't want to have to inject the epics via import, you can also dynamically inject epics The docs detail a way to inject the epic asynchronously, meaning that instead of file injection you could include it as part of the action body during start up, this might make testing a bit easier.
const appLoadEpic = (action$, state$) => {
// Listen for the load app request before subscribing to the initialization
action$.pipe(
ofType(LOAD_APP_REQUEST),
// Now the epic is injected during the app loading, and you run it inline
// here. This makes it easy to mock it during testing
mergeMap(({epic}) => epic(action$, state$).pipe(endWith({type: LOAD_APP_SUCCESS}))),
)
}

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.

RxJS - retry or reset

Let's say I have a sequence like this:
Rx.Observable
.interval(1000)
.subscribe(data => {console.log(data)})
With operators, how can I 'restart' the sequence, meaning unsubscribe and resubscribe.
The real scenario is that the sequence is a socket stream, upon certain conditions we need to unsubscribe and resubscribe, kind of like the retryWhen(errors) works, but not with errors...would ideally be something like...retryWhen(bool:Subject).
I'd do it using switchMap() because it automatically unsubscribes from the old Observable and subscribes to the new one. In this case we'll use only .switchMap(() => source):
const subject = new Subject();
const source = Observable.create(obs => {
console.log('Observable.create');
obs.next(42);
});
subject.switchMap(() => source)
.subscribe(v => console.log('next:', v));
setTimeout(() => subject.next(), 1000);
setTimeout(() => subject.next(), 5000);
This prints the following:
Observable.create
next: 42
Observable.create
next: 42
Just instead of source you'll have your WebSocket source (or whatever you have).

Resources