November 21, 2024

Login component with NgRx in Angular 6

Hey there! The purpose of this post is to show how we can create a login component using NgRx which shows an error message if login failed or a success message if it succeeds. I will show you how to setup NgRx, how to design the actions and effects. Unit tests are very easy to implement as well if we follow the NgRx approach.

What is NgRx?

NgRx is a very useful library similar to Redux which does state management of the app. Everything an app needs is saved in a big state object. Modules in Angular have their own state objects which are part of this big state NgRx is managing. More than that, when something changes, interested parties are automatically notified via a system called selectors which actually provide Observable instance you can attach to. There’s also an action dispatch mechanism. Any change in the state must be performed by an action. This is the list of the main components of NgRx:

  • Store: core state management
  • Actions: an object that, when dispatched, can change the state of the app
  • Effects: side-effects that happen after an action is dispatched so that it performs operations (eg. call an API, write to localStorage) or chain other events
  • Selectors: observable objects that trigger events when something in the state changes

Install NgRx

NgRx is composed of more npm packages. For our sample, we’ll not need all of them:

$ yarn add @ngrx/store @ngrx/effects @ngrx/schematics @ngrx/store-devtools

Initial Setup

We first generate the Login component, then we wire up the NgRx library:

$ ng generate component Login
CREATE src/app/login/login.component.html (24 bytes)
CREATE src/app/login/login.component.spec.ts (621 bytes)
CREATE src/app/login/login.component.ts (265 bytes)
CREATE src/app/login/login.component.css (0 bytes)
UPDATE src/app/app.module.ts (392 bytes)
$ ng generate @ngrx/schematics:store State --root --module app.module.ts
CREATE src/app/reducers/index.ts (359 bytes)
UPDATE src/app/app.module.ts (729 bytes)
$ ng generate @ngrx/schematics:action LoginAction
CREATE src/app/login-action.actions.ts (297 bytes)
$ ng generate @ngrx/schematics:effect App --root --module app.module.ts
CREATE src/app/app.effects.ts (182 bytes)
CREATE src/app/app.effects.spec.ts (571 bytes)
UPDATE src/app/app.module.ts (861 bytes)

So far, we created the main state of the application, the login action and the application effects.

Creating the LoginState

In the login state, we need to store 2 facts: 

  1. Whether the login has been attempted or not
  2. Whether the login attempt was successful or not
export interface LoginState {
  loginAttempted: boolean;
  loginSuccess: boolean;
}

Now we need to create the actions needed for the login to work:

  1. LoginAction: it’ll be dispatch when the user hits login button
  2. LoginResult: this will be the result coming as an effect of LoginAction
  3. ClearResult: if dispatched, this will clear the result of the login
export enum LoginActionTypes {
  AttemptLogin = '[LoginAction] AttemptLogin',
  LoginResult = '[LoginAction] LoginResult',
  ClearResult = '[LoginAction] ClearResult',
}

export class AttemptLogin implements Action {
  readonly type = LoginActionTypes.AttemptLogin;
  constructor(public username: string, public password: string) {
  }
}

export class LoginResult implements Action {
  readonly type = LoginActionTypes.LoginResult;
  constructor(public success: boolean) {
  }
}

export class ClearResult implements Action {
  readonly type = LoginActionTypes.ClearResult;
}

export type LoginActions = AttemptLogin | LoginResult | ClearResult;

Writing the reducer

Basically, a reducer is a function that receives the current state and one action. Based on the action type, it’ll decide which operations to perform on the state object. After the operations are done, it returns the new state object. This is how our reducer looks like:

export function reducer(state = initialState, action: LoginActions): LoginState {
  switch (action.type) {
    case LoginActionTypes.AttemptLogin:
      return state;
    case LoginActionTypes.LoginResult:
      const result = action as LoginResult;
      return {
          ...state,
          loginAttempted: true,
          loginSuccess: result.success
      }
    case LoginActionTypes.ClearResult:
      return {
          ...state,
          loginAttempted: false,
          loginSuccess: false,
      }
    default: {
        return state;
    }
  }
}

If you notice, the AttemptLogin action doesn’t mutate the state in any way. This is because for login we would use a service which performs HTTP calls and this one will be implemented in an effect. As a general rule, don’t ever call an asynchronous function inside a reducer!

The LoginService

We will now create a login service which in real scenarios will call an API do perform the login. 

$ ng generate service Login
CREATE src/app/login.service.spec.ts (328 bytes)
CREATE src/app/login.service.ts (134 bytes)

It’ll return an Observable which will emit a boolean value representing the status of the request. 

@Injectable({
  providedIn: 'root'
})
export class LoginService {

  constructor() { }

  login(user: string, password: string): Observable<boolean> {
    if (user == 'test' && password == 'test') {
      return of(true)
    } else {
      return of(false)
    }
  }
}

In our sample, only if the user/pass are test/test, the authentication will work!

Creating the login effect

The login effect will trigger only when a login attempt is performed. It takes the AttemptLogin action and calls the login service with the given username and password. Once this service is called, the result is then encapsulated into the LoginResult action. After the LoginResult action is dispatched, the reducer is called automatically, hence modifying  the state with the result of the login. The subscribed selectors will be as well notified about this change.

@Injectable()
export class AppEffects {

  constructor(private actions: Actions, private loginService: LoginService) {}

  @Effect()
    login = this.actions.ofType<AttemptLogin>(LoginActionTypes.AttemptLogin)
        .pipe(
            exhaustMap(auth => 
                this.loginService.login(auth.username, auth.password)
                    .pipe(
                        map(success => new LoginResult(success))
                    )
                )
        )
}

The final login component

Sometimes it’s quite tough to write this kind of code, but it’s nevertheless important because it provides a clearer separation of concerns, you can put business logic out of the components, the app is much more maintable and it’s easier to write unit tests!

Now this is what the final login component should do:

  1. Display two inputs for username and password
  2. It will show a green message if login is success, a red one otherwise
  3. There will be a clear button only visible if the login was attempted which clears the attempt
No login was attempted
Login is successful
Login failed

Conclusions

I provided a lot of code here and I won’t paste the code for the login component. Still the functionality should be clear. As you can see, this approach with NgRx helps us to have a better maintainability of the application with the only downside of writing some boilerplate code in the beginning. I was too lazy to write unit tests now but they can be written very easily for both effects and reducers. Combined with component testing you can be assured that your app is fine. Here’s the link to the full source code:

Source code on Github

Thanks for reading, I hope you found this article useful and interesting. If you have any suggestions don’t hesitate to contact me. If you found my content useful please consider a small donation. Any support is greatly appreciated! Cheers  😉

afivan

Enthusiast adventurer, software developer with a high sense of creativity, discipline and achievement. I like to travel, I like music and outdoor sports. Because I have a broken ligament, I prefer safer activities like running or biking. In a couple of years, my ambition is to become a good technical lead with entrepreneurial mindset. From a personal point of view, I’d like to establish my own family, so I’ll have lots of things to do, there’s never time to get bored 😂

View all posts by afivan →