Create a Login Feature with NgRx in 6 steps

Zoe Koulouris Augustinos
Upstate Interactive
5 min readDec 17, 2018

Want to create a full feature with NgRx, including an API call? Logins are common enough. Follow along as I go through each step involved in adding NgRx to your login form — including successfully updating your state with the response.

  1. It all starts with Actions.

Let’s set up some strongly typed actions in actions/user/user.actions.ts file. Keep the syntax consistent, especially for API calls:

  • ACTION-NAME: used for the initial dispatch
  • ACTION-NAME_COMPLETE: used to update the state with the success response payload from the server
  • ACTION-NAME_ERROR: used to update the state with the error response payload from the server

Be aware of whether a payload is necessary for the action. If so, give it an appropriate Type. In the example, I’m importing the User model for LOGIN and LOGIN_COMPLETE.

import { Action } from '@ngrx/store';
import { User } from '../../models/user';
export enum UserActionTypes {
LOGIN = '[User] LOGIN',
LOGIN_COMPLETE = '[User] LOGIN_COMPLETE',
LOGIN_ERROR = '[User] LOGIN_ERROR',
}
export class Login implements Action {
readonly type = UserActionTypes.LOGIN;
constructor(public payload: User) {}
}
export class LoginComplete implements Action {
readonly type = UserActionTypes.LOGIN_COMPLETE;
constructor(public payload: User) {}
}
export class LoginError implements Action {
readonly type = UserActionTypes.LOGIN_ERROR;
constructor(public payload: any) {}
}
export type UserActions
= Login
| LoginComplete
| LoginError;

2. Dispatch your action in a smart component.

In this case, I’m emitting an event from my presentational login component when the user submits the login form up to the login page (aka smart component). You can learn more about smart and presentational components here.

In your Login Page template, src/app/auth/containers/login-page/login-page.component.html pass up the event emitter from the presentational component.

<app-login (login)="onLogin($event)"></app-login>

In your Login Page ts file, src/app/auth/containers/login-page/login-page.component.ts add the event emitter function.

onLogin(form) {
this.store.dispatch(new Login(form));
}

3. Set up the login API function in your ApiService

I typically keep function names consistent by naming them according to their type and purpose. Also, make sure to pass the parameter in with the correct Type.

src/app/services/api.service.ts

postUserLogin(user: User) {
return this.http.post(`${this.apiURL}/auth/login`, user)
.pipe(
tap((res: User) => {
// authentication and local storage code can go here
})
);
}

4. Create your NgRx Effect for Login

Effects are used to handle the “side effects” of Actions. RxJs methods are used to connect the GET action to the API function, and then dispatch a LoginComplete or LoginError to the reducer.

In this case, I’m using mergeMap to dispatch my (User) LoginComplete Action and my (Locations) GetComplete immediately upon login. This way I can flatten my Observable but have the data for both. I find this to be common with my Logins, but if it’s not necessary, just use Map to dispatch the LoginComplete Action.

src/app/effects/user/user.effects.ts

@Effect()postLogin$: Observable<Action> = this.actions$.pipe(
ofType<Login>(UserActionTypes.LOGIN),
switchMap((action) => {
return this.api.postUserLogin(action.payload)
.pipe(
mergeMap((user: any) => [
new LoginComplete(user),
new GetComplete(user.locations),
]
],
),
catchError(errorHandler(LoginError))
);
}),
);

5. Reducers files

Create a User reducer folder and file, and set up your initial state to include your User type and any additional keys that you’d like to update on your state during Login. Some common additions include keys for a loading indicator and error message. In your Reducer functions, you can update the keys that you added to the initial state. There are some examples below.

src/app/reducers/user/user.reducer.ts

export interface State {
user: User;
loggedIn: boolean;
isLoading: boolean;
errorMessage: string;
hasError: boolean;
}

On LOGIN, the User state will be the same as initial state, but you’ll want to set the loading indicator value to true.

If the Login has a successful server response, your ApiService and User Effects should return a User payload. LOGIN_COMPLETE state should be updated with the action.payload and loading indicator should be false.

If the Login has a server error, your ApiService and User Effects should return an Error payload. LOGIN_ERROR state should be updated with the error message.

Don’t forget to export out slices of your User state to create selectors in your reducer root.

export function reducer(state = initialState, action: UserActions | LocationActions): State {switch (action.type) {case UserActionTypes.LOGIN:
return {
...state,
hasError: false,
errorMessage: null,
isLoading: true
};
case UserActionTypes.LOGIN_COMPLETE:
return {
...state,
loggedIn: true,
user: action.payload,
isLoading: false,
};
case UserActionTypes.LOGIN_ERROR:
return {
...state,
errorMessage: action.payload,
hasError: true,
isLoading: false,
};
default:
return state;
}
}export const getLoggedIn = (state: State) => state.loggedIn;
export const selectUser = (state: State) => state.user;
export const errorMessage = (state: State) => state.errorMessage;
export const hasError = (state: State) => state.hasError;
export const isLoading = (state: State) => state.isLoading;

Check out your Redux Devtools to see the payloads carried by each action and how the reducers update your State.

In your reducer root src/app/reducers/index.ts you’ll import the User reducer. This is where we can create selectors to access slices of state from the Store throughout your components. Here are a few example selectors for the Login.

export const selectUserState = (state: State) => state.user;
export const selectCurrentUser = createSelector(
selectUserState,
fromUser.selectUser
);
export const getLoggedIn = createSelector(
selectUserState,
fromUser.getLoggedIn
);
export const userErrorMessage = createSelector(
selectUserState,
fromUser.errorMessage
);
export const userHasError = createSelector(
selectUserState,
fromUser.hasError
);
export const userIsLoading = createSelector(
selectUserState,
fromUser.isLoading
);

6. Use selectors in Smart components.

You can use your selectors in your components to make changes in the app like updating the view, routing the page, etc.

In the example page below, you can subscribe to this.complete$ to route the page when LoginComplete dispatches. You can also subscribe to this.isLoading$ to add a loading indicator to the view, or this.errorMessage$ to add the error message to the view.

export class LoginPageComponent implements OnInit, OnDestroy {
complete$: Observable<any>;
isLoading$: Observable<any>;
errorMessage$: Observable<any>;
hasError$: Observable<any>;
private unsubscribe: Subject<void> = new Subject();
constructor(
private store: Store<fromRoot.State>,
private actions$: CMSActions,
private router: Router,
private zone: NgZone,
) {
this.complete$ = this.actions$.ofType(UserActionTypes.LOGIN_COMPLETE);
this.isLoading$ = this.store.select(fromRoot.userIsLoading);
this.errorMessage$ = this.store.select(fromRoot.userErrorMessage);
this.hasError$ = this.store.select(fromRoot.userHasError);
}
ngOnInit() {
this.complete$
.pipe(
takeUntil(this.unsubscribe)
)
.subscribe(() => {
this.router.navigate(['../../../dashboard/home']);
});
}
onLogin(form) {
this.store.dispatch(new Login(form));
}
ngOnDestroy() {
this.unsubscribe.next();
this.unsubscribe.complete();
}
}

The right software can change EVERYTHING. Interested in learning more?

--

--

Zoe Koulouris Augustinos
Upstate Interactive

Entrepreneur / Software Developer / Health enthusiast — Cofounder, Upstate Interactive & Women in Coding