ngrx effect - waiting on external data - asynchronous

I have an effect which runs when a user is downloaded and stored in state:
getMetadata$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromUserSelectors.getUserSuccess),
concatLatestFrom(() => this.store.select(fromProductSelectors.selectProduct)),
exhaustMap(([user, product]) =>
// do something with user and product
)
);
});
This effect is triggered when getUserSuccess is dispatched.
However, you can see on the 4th line, I also need the product which is stored in a different store and is downloaded asynchronously to the user being downloaded. So it isn't necessarily populated until later.
How can I tell this effect to wait for the product to be populated before resuming? I do not want to retrigger the effect. I thought that was the purpose of concatLatestFrom introduced in ngrx v12, but maybe I am misunderstanding it.
I am aware I could have multiple actions being listened to in my ofType call and listen to fromProductSelectors.getProductSuccess, but I only want this effect to run when getUserSuccess is called, not fromProductSelectors.getProductSuccess too, but need the selectProduct to not be undefined.

You can use the filter operator to wait until it becomes available.
getMetadata$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromUserSelectors.getUserSuccess),
concatLatestFrom(() => this.store.select(fromProductSelectors.selectProduct).pipe(filter(x => !!x)),
exhaustMap(([user, product]) =>
// do something with user and product
)
);
});

Related

ngrx - effect for group of actions

i have a store with few actions: play, stop, clear, change
all of the actions have effects and they work great.
my question is, is there a way to create a single effect for this group? if one of this actions is dispatched, i want a separate effect that sends a new action(btnsStateChaned)
i don't want to add my new action in every effect
i want something to replace all the || operators(they don't work as well):
updateBtnsState$: Observable<Action> = createEffect(() =>
this.actions$.pipe(
ofType(gridStateActions.Play || gridStateActions.Stop || gridStateActions.ClearActive || gridStateActions.ChangeVideo),
map(_ => new gridStateActions.UpdateButtonsState())
)
);
UPDATE
updateBtnsState$: Observable<Action> = createEffect(() =>
this.actions$.pipe(
ofType(gridStateActions.GRID_ACTIONS.PLAY, gridStateActions.GRID_ACTIONS.STOP, gridStateActions.GRID_ACTIONS.CLEAR_ACTIVE, gridStateActions.GRID_ACTIONS.CHANGE_VIDEO),
map(_ => new gridStateActions.UpdateButtonsState())
)
);
this is working, still, i would like to know if there is a better way
that's exactly the way as you specified in your question:
ofType(
gridStateActions.GRID_ACTIONS.PLAY,
gridStateActions.GRID_ACTIONS.STOP,
gridStateActions.GRID_ACTIONS.CLEAR_ACTIVE,
gridStateActions.GRID_ACTIONS.CHANGE_VIDEO,
),
and there's no other official way to do it.

Dispatch action in effect before return statement

I need to dispatch/trigger another ui-related action inside an effect before calling the service to fetch data without injecting the store
I managed to fix it by injecting the store in the constructor and dispatch this extra action in effect right before calling the service for fetching the data(this.store.dispatch(UIActions.startLoading()) but I am not sure if injecting the store in the effect is a good practice
recipes$ = createEffect(() => this.actions$.pipe(
ofType(RecipeActions.FETCH_RECIPES),
switchMap(() => this.recipeService.getRecipes().pipe(
switchMap(recipes => [
RecipeActions.setRecipes({ recipes }),
UIActions.stopLoading()
]),
catchError(() => EMPTY)
))
));
I wonder if there is a way to do this like using
tap(() => of(UIActions.startLoading())) inside the first switchMap
You're correct in the use of tap operator for your desired functionality.
However, you need to do store.dispatch instead of simply returning an of observable.
Also, instead of multiple switchMap, you can use the from observable to return array of actions.
recipes$ = createEffect(() => this.actions$.pipe(
ofType(RecipeActions.FETCH_RECIPES),
tap(()=> this.store.dispatch(UIActions.startLoading())),
switchMap(() => this.recipeService.getRecipes().pipe(
map(recipes => from([
RecipeActions.setRecipes({ recipes }),
UIActions.stopLoading()
]),
catchError(() => EMPTY)
))
));

How to emit one event after finishing previous one using NgRx?

I am new to this technology and I am working on a project where blow's the scenario.
table schema:
tbl.Product
id
product name
brand_id [FK]
tbl.Brand
id
BrandName
in this, while submitting I am adding BrandName first and taking the newly added id of brand record and insert full record in product table with brand_id.
Submit(){
this.saveBrand.emit(brand);
product.brand_id = brands.id;
this.saveProduct.emit(product);
}
for this, I am using NgRx Store.
Brand.effects.ts
addBrand$: Observable<Action> = this.actions$
.pipe(ofType<Add>(BrandActionTypes.Add),
switchMap(action => this.BrandService.addBrand(action.payload)),
map((Brand: any) => new AddSuccess(Brand)),
catchError(err => {
toastr.error(`Could not add Brand.`);
console.error(err);
return of(new AddFail(err));
})
);
Product.effects.ts
addProduct$: Observable<Action> = this.actions$
.pipe(ofType<Add>(ProductActionTypes.Add),
switchMap(action => this.ProductService.addProduct(action.payload)),
map((Product: any) => new AddSuccess(Product)),
catchError(err => {
toastr.error(`Could not add Product.`);
console.error(err);
return of(new AddFail(err));
})
);
while executing this both event emitting together (both are not async). so product is not getting brand_id.
i am expecting output like.
Submit(){
this.saveBrand.emit(brand);
product.brand_id = brands.id;
this.saveProduct.emit(product); \\hold until brand get added
}
Dispatching an action is an async process, that's why the brand id isn't "populated".
To solve your problem you can do a few things:
your products effects can listen on the AddSuccess action
use one effect to handle both service calls
make on service call?
A great article about some of the patterns NgRx: Patterns and Techniques

When using redux-observable, from a single action and single stream, how do I dispatch different actions based on filter

Update
Here is a working example using redux-observable. https://redux-observable-playground-ykzsyp.stackblitz.io This achieves what I want using mergeMap and if/else statement, but I was hoping to use Observable.filter as that seems more elegant.
Original question
I have an epic that currently dispatches a single action but would like to dispatch different actions based on a filter using a single stream. Here is the current code:
const streamEpic = (action$) => action$.pipe(
ofType('START_PLAYBACK_STREAM'),
switchMap(playbackStream$),
filter((playEvent) => playEvent.type === 'LOAD')),
map((playEvent) => loadActionCreator(playEvent.fileName))
// how can I filter on other types?
// and dispatch other actions?
);
I've seen many rxjs examples that use a single stream and filter to map different actions, for example:
playbackStream$
.filter((playEvent) => playEvent.type === 'LOAD'))
.map((playEvent) => loadActionCreator(playEvent.fileName));
playbackStream$
.filter((playEvent) => playEvent.type === 'START'))
.map((playEvent) => startActionCreator());
playbackStream$
.filter((playEvent) => playEvent.type === 'STOP'))
.map((playEvent) => stopActionCreator());
I'm trying to do this same thing in redux-observable but no luck. If I use tap, ignoreElements, and store.dispatch I can get the following to work but I know its an anti-pattern.
const streamLoadEvents = (action$, store) => action$.pipe(
ofType('START_PLAYBACK_STREAM'),
tap(() => {
playbackStream$
.filter((playEvent) => playEvent.type === 'LOAD'))
.map((playEvent) => store.dispatch(loadActionCreator(playEvent.fileName)));
playbackStream$
.filter((playEvent) => playEvent.type === 'START'))
.map((playEvent) => store.dispatch(startActionCreator()));
playbackStream$
.filter((playEvent) => playEvent.type === 'STOP'))
.map((playEvent) => store.dispatch(stopActionCreator()));
}),
ignoreElements()
);
I know that I could also use a switch or if/else statement inside of something like map or switchMap, like the answer here: Redux-Observable multiple actions in single epic, but I'd like to avoid this as its rather inelegant and does not take full advantage of streaming operators. The answer here: https://stackoverflow.com/a/40895613/367766 seems to get me a little closer...
What's the suggested approach here? Are the operators or example you can point me to? Thanks!
You're right, if/else should only be used as a last resort and there are probably several ways you could approach this, in any case, here's my take on this:
There's a merge method which can be viewed as a reactive equivalent of the if/else operator: you provide it with observables, and if any of them emits, the resulting observable will emit it's own value(AC) as well. When a new action comes through, we inspect it, and pick a new action creator(AC) accordingly. Let's take look at some code:
const filteredAction$ = action$.pipe(
ofType('START_PLAYBACK_STREAM'),
);
Nothing interesting here, just filtering out the action types we're not interested in, and then
const operation$ = merge(
filteredAction$.pipe(
filter((action) => action.payload.type === 'LOAD'),
mapTo(loadActionCreator),
),
filteredAction$.pipe(
filter((action) => action.payload.type === 'START'),
mapTo(startActionCreator),
),
filteredAction$.pipe(
filter((action) => action.payload.type === 'STOP'),
mapTo(stopActionCreator),
),
).pipe(
startWith(startActionCreator)
);
Here we are choosing which action creator to use based on the action's payload (please note that I'm following the flux standard for actions - the action's data is under the payload property now, there's an awesome library you could use as a helper for it. Also, you should probably rename the 'type' prop to avoid confusion).
mapTo basically tells the operation$ stream what to emit, you can think of this expression as assigning a function to some variable for later use. Every time an epic gets invoked, we'll choose a proper action creator for it here.
startWith is actually not needed here (assuming the first action always includes
the 'START' type) and is for semantic purposes only.
Last but not least, an epic has to return at least one action for dispatching:
return combineLatest(
operation$,
filteredAction$
)
.pipe(
map((arg) => {
const operation = arg[0];
const actionObject = arg[1];
return operation(actionObject);
})
)
Latest values from both streams are combined together to form a follow up action: operation$ (which yields us a proper action creator function) and filteredAction$ (always contains the action itself, we might need some data from it).

Is it possible to both dispatch an array of actions and also navigate from an ngrx effect?

I have an issue with one of my application's ngrx effects. I am basically trying to execute multiple actions using concatMap() AND navigate using the router store's go().
Here is the effect:
#Effect()
loadPersonalInfoAndSignin$: Observable<Action> = this.actions$
.ofType(session.ActionTypes.LOAD_PERSONAL_INFO)
.map((action: LoadPersonalInfoAction) => action.payload)
.do(sessionToken => {
localStorage.setItem('authenticated', 'true');
localStorage.setItem('sessionToken', sessionToken);
})
.switchMap(() => this.userAccountService
.retrieveCurrentUserAccount()
.concatMap(currentUserAccount => [
new LoadUserAccountAction(currentUserAccount),
new SigninAction(),
new LoadMessagesAction({})
])
)
.mapTo(go(['/dashboard']));
If I remove the .mapTo(go(['/dashboard'])), then all three actions in the concatMap array are successfully dispatched to their corresponding effects.
I am therefore wondering why my mapTo(go(... is causing the last two actions in the array (i.e. SigninAction & LoadMessagesAction) not to be dispatched to their corresponding effects..
Can someone please help?
edit: Changing mapTo to do as follows:
.do(go(['/dashboard']));
results in the following error:
ERROR in /Users/julien/Documents/projects/bignibou/bignibou-client/src/app/core/store/session/session.effects.ts (55,9): Argument of type 'Action' is not assignable to parameter of type 'PartialObserver<SigninAction>'.
Type 'Action' is not assignable to type 'CompletionObserver<SigninAction>'.
Property 'complete' is missing in type 'Action'.
Using do for the go call will not see the route changed. go is an action creator and the action that it creates needs to be emitted from the effect for #ngrx/router-store to receive the action and effect the route change.
Also, the mapTo operator will ignore what it receives and will emit the value you've specified, so it's not appropriate, either.
Instead, you should include the action created by the go call in your concatMap array:
#Effect()
loadPersonalInfoAndSignin$: Observable<Action> = this.actions$
.ofType(session.ActionTypes.LOAD_PERSONAL_INFO)
.map((action: LoadPersonalInfoAction) => action.payload)
.do(sessionToken => {
localStorage.setItem('authenticated', 'true');
localStorage.setItem('sessionToken', sessionToken);
})
.switchMap(() => this.userAccountService
.retrieveCurrentUserAccount()
.concatMap(currentUserAccount => [
new LoadUserAccountAction(currentUserAccount),
new SigninAction(),
new LoadMessagesAction({}),
go(['/dashboard'])
])
);

Resources