cmatthews.dickson
cmatthews.dickson

Reputation: 697

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

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...

Upvotes: 37

Views: 13754

Answers (4)

Leon Radley
Leon Radley

Reputation: 7682

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.

Upvotes: 20

Laurens
Laurens

Reputation: 1031

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
            });
        }
        // ...
    }
}

Upvotes: 2

littleStudent
littleStudent

Reputation: 127

Scenario 1

option A & B are both good to go i think, but maybe option A is a bit too much just for navigation! You could also directly use the router in your LoginActions.LOGIN if you consider the navigation as part of this side-effect.

Scenario 2

There is nothing wrong with chaining up @Effects. Dispatch an action (SET_1_REQUEST) that triggers your effect A. effect A returns an action (SET_1_SUCCESS) that gets picked up by your reducer (now you can display that status to the user) and it gets picked up by effect B.

Hope this makes sense.

Upvotes: 0

Derek Kite
Derek Kite

Reputation: 1825

I'm learning this stuff as well.

What if you dispatched to the reducer when the first in the chain was done?

Something like this:

const myReducer = (state,action:Action) => {
switch action.type {
case : "my_first_step" : runfirststep(action.payload);
case : "my_second_step": runsecondstep(action.payload);
....

function runfirststep(data) {
...
//bunch of stuff to do,
//update something to display
store.dispatch('my_second_step');
}

etc.

The ngrx example does this in the effects file. When the add or update, they call UPDATE_COMPLETE or something similar presumably so some notification can be done.

I'm slowly grasping how big the reducers will end up being in a moderately complex app.

Upvotes: -1

Related Questions