Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to perform multiple related operations/effects/actions with ngrx/effects

Tags:

ngrx

I am working on an application that is using ngrx/store 1.5 along with thunk middleware and I am attempting to move to ngrx/store 2.0 and ngrx/effects. I have a couple questions with regards to how to handle multiple related actions and/or effects.

I realize that the "mindset" for thunks vs effects is different and I'm trying to get my head around the differences. I have looked through the available example apps, and haven't found anything that seems to fit what I'm attempting, so perhaps I'm still approaching it completely wrong.

Scenario 1

Here is a side-effect to handle making the request to the server for a login:

@Effect login$: any = this.updates$     .whenAction(LoginActions.LOGIN)     .map(toPayload)     .switchMap(payload =>          this.loginService.login(payload.user, payload.password)             .map(result => this.actions.loginSuccess(value))             .catch((error) => Observable.of(this.loginError(error))) )); 

Given that initial side-effect, what would be the "correct" or "suggested" way to trigger navigation to a "home" screen upon successful login? This could also be generalized to simply triggering a sequence of actions or operations.

A few options I have considered:

(a) Another effect triggered by login success, that fires a subsequent action to trigger navigation?

@Effect navigateHome$: any = this.updates$     .whenAction(LoginActions.LOGIN_SUCCEEDED)     .mapTo(this.actions.navigateHome()); 

(b) Another effect triggered by login success, that simply performs the navigation operation?

@Effect navigateHome$: any = this.updates$     .whenAction(LoginActions.LOGIN_SUCCEEDED)     .do(this.navigateHome())     .filter(() => false); 

(c) Concatenating an additional action to those emitted by the initial login effect? (sample obviously not quite correct, but gives the idea)

@Effect login$: any = this.updates$     .whenAction(LoginActions.LOGIN)     .map(toPayload)     .switchMap(password => Observable.concat(         this.loginService.login(passcode)             .map(result => this.actions.loginSuccess(value))             .catch((error) => Observable.of(this.loginError(error))),         Observable.of(this.actions.navigateHome())     )); 

(d) Other?

Scenario 2

Consider a case where a number of requests need to be made in sequence, and as each request begins we want to update the "status" so that feedback can be provided to the user.

Example of a thunk for something along those lines:

multiphaseAction() {     return (dispatch) => {         dispatch(this.actions.updateStatus('Executing phase 1');         this.request1()             .flatMap(result => {                 dispatch(this.actions.updateStatus('Executing phase 2');                 return this.request2();             })             .flatMap(result => {                 dispatch(this.actions.updateStatus('Executing phase 3');                 return this.request3();             })             ...     } } 

Again, what would be the "correct" or "suggested" way to go about this in using the effects approach?

This one I'm more stuck on, not really sure what could be done other than adding some .do(this.store.dispatch(this.actions.updateStatus(...)) somehow...

like image 291
cmatthews.dickson Avatar asked Jun 04 '16 18:06

cmatthews.dickson


People also ask

How do you call an effect in NgRx?

You need to run this command npm install @ngrx/effects — save to install required dependencies. You have to define the actual services to make the API calls. Finally, you need to register all the effects with the EffectsModules and import this module into your main module or feature module.

How does NgRx effect work?

Most effects are straightforward: they receive a triggering action, perform a side effect, and return an Observable stream of another action which indicates the result is ready. NgRx effects will then automatically dispatch that action to trigger the reducers and perform a state change.

What is ofType NgRx?

'ofType' filters an Observable of Actions into an observable of the actions whose type strings are passed to it.


2 Answers

The answer for the navigation scenario is your b answer

@Effect navigateHome$: any = this.updates$     .whenAction(LoginActions.LOGIN_SUCCEEDED)     .do(this.router.navigate('/home'))     .ignoreElements(); 

Explanation: You react to the LOGIN_SUCCESS, and because the router doesn't return a new action, we need to stop the propagation of the stream, which we do by filtering everything.

If you forget to filter, the router returns undefined, which in turn will lead to the reducer to reduce an undefined value, which usually results in a null pointer when it tries to read the type of the action

Another way to solve it is to use https://github.com/ngrx/router-store

Check the documentation on how to add router-store to your app.

The same effect will now look like this.

import { go } from '@ngrx/router-store';  @Effect navigateHome$: any = this.updates$     .whenAction(LoginActions.LOGIN_SUCCEEDED)     .map(() => go(['/home'])); 

The go action will dispatch a router action that the router reducer will pick up and trigger a route change.

like image 75
Leon Radley Avatar answered Oct 07 '22 02:10

Leon Radley


Scenario 1

Consider whether or not navigateHome should change the state. Also, whether or not this navigateHome action is dispatched from other places to achieve the same thing. If so, returning an action is the way to go. Therefore, option A.

In some cases option B might make more sense. If navigateHome only changes the route, it might be worth considering.

Side-note: you could use ignoreElements here instead of the filter(() => false).

Scenario 2

I would suggest chaining your actions here in multiple effects and giving feedback by modifying the state in the reducer for every action.

For example:

@Effect() trigger$: Observable<Action> = this.updates$     .whenAction(Actions.TRIGGER)     .do(() => console.log("start doing something here"))     .mapTo(Actions.PHASE1);  @Effect() phase1$: Observable<Action> = this.updates$     .whenAction(Actions.PHASE1)     .do(() => console.log("phase 1"))     .mapTo(Actions.PHASE2);  @Effect() phase2$: Observable<Action> = this.updates$     .whenAction(Actions.PHASE2)     .do(() => console.log("phase 2"))     .ignoreElements(); 

And give feedback by modifying the state:

function reducer(state = initialState, action: Action): SomeState {     switch (action.type) {         case Actions.TRIGGER: {             return Object.assign({}, state, {                 triggered: true             });         }         case Actions.PHASE1: {             return Object.assign({}, state, {                 phase1: true             });         }         case Actions.PHASE2: {             return Object.assign({}, state, {                 phase1: false,                 phase2: true             });         }         // ...     } } 
like image 23
Laurens Avatar answered Oct 07 '22 01:10

Laurens