I am new to ngrx and Redux style architecture and I am having problems understanding how I should chain actions / effects. One example is achieving basic functionality such as taking an action after a user logs in. My core struggle here is that the action to be taken after the user has logged in will vary depending upon the current state of the application - the user may be anywhere in the application when presented with a login prompt / page.
Any examples I have seen hardcode effects which will happen once the user has logged in. In my scenario this does not fit well as stated above I do not always want the same action to happen.
Here is some example code of a home page which includes a login component. In this scenario I would like the user to be redirected to '/buy' once they have logged in.
@Component(..)
export class HomePageComponent {
constructor(private store: Store<any>) {
}
public onLoginSubmitted(username, password) {
this.store.dispatch(new loginActions.Login({username, password}));
// once this has happened successfully navigate to /buy
}
}
Example effect
@Injectable()
export class LoginEffects {
......
@Effect()
login$ = this.actions$
.ofType(loginActions.ActionTypes.LOGIN)
.switchMap((data) => {
return this.loginService.login(data.payload)
.map(() => new loginActions.LoginSuccess({loggedIn: true, isLoggingIn: false}))
.catch((error) => {
return of(new loginActions.LoginError({loggedIn: false, isLoggingIn: false}));
});
});
......
}
I have several thoughts on how you might solve this problem - but none of them feel right. These include
Can anyone point me down the right path / present examples of how this should be solved?
An effect can fire off multiple actions using mergeMap
on an array of actions. What I would usually do is, pass in the additional actions that I would like the login effect to also dispatch.
@Component(..)
export class HomePageComponent {
constructor(private store: Store<any>) { }
public onLoginSubmitted(username, password) {
this.store.dispatch(new loginActions.Login({
username,
password,
onCompleteActions: [new loginActions.Redirect({ route: '/buy' })] }));
}
}
The login effect would then dispatch any passed-in actions using mergeMap
. You would still have to create an effect for each action, but now you can build more reusable actions, such as a redirect action.
@Injectable()
export class LoginEffects {
......
@Effect()
login$ = this.actions$
.ofType(loginActions.ActionTypes.LOGIN)
.switchMap((data) => {
let mappedActions = [new loginActions.LoginSuccess({loggedIn: true, isLoggingIn: false})];
if (data.payload.onCompleteActions)
mappedActions = mappedActions.concat(data.payload.onCompleteActions);
return this.loginService.login(data.payload)
.mergeMap(mappedActions)
.catch((error) => {
return of(new loginActions.LoginError({loggedIn: false, isLoggingIn: false}));
});
});
......
}
What I like about this solution is that it is scalable into the future, since any actions can be passed in, and there's no switch or if-else tree to decide what to do. You can also pass in multiple actions, so there's no need to build a 'redirectAndCloseModal' action, you could pass in redirect
as well as closeModal
.
this.store.dispatch(new loginActions.Login({
username,
password,
onCompleteActions: [new loginActions.Redirect({ route: '/buy' }), new modalActions.CloseModal()] }));
You can read a bit more about dispatching multiple Actions from one @Effect() at this github issue.
Edit: Regarding you comments, it's not just route changes that may make you want to stop the login, it could be anything, such as closing a modal, and there's no generic way to watch for that. You could add something to your store such as a runLoginActions
boolean, and then read that data from your store after the login request comes back.
Then in your login$
effect, you could read that from the store to decide to run the additional actions or not.
@Effect()
login$: Observable<Action> = this.actions$
.ofType(auth.actionTypes.LOGIN)
.map((action: auth.LoginAction) => action.payload)
.switchMap((user) => {
let successAction = new loginActions.LoginSuccess({loggedIn: true, isLoggingIn: false});
let mappedActions = [successAction];
if (data.payload.onCompleteAction)
mappedActions.push(data.payload.onCompleteAction);
return this.authService.login(user.username, user.password)
.withLatestFrom(this.store)
.mergeMap(([result, store]) => store.runLoginActions ?
[mappedActions] :
[successAction]
)
.catch((err) => {
try {
return of(new auth.NetworkFailureAction(err[0].code));
} catch (e) {
throw `Array not in the expected format of [{ code: "", message: "" }]`;
}
});
});
Note that in your reducer for loginSuccess
, you should set your runLoginActions = true
, and it should start as true by default, only to be turned off when a route is changed, or a component is destroyed.
Keep in mind this is a general solution, and you're going to need to add the actions yourself, update your reducers, etc.
It's a more of a manual process, but since your cases for not wanting to perform actions are pretty specific, it likely has to be done this way.