Angular

State Management in Angular Using NGXS - Part 2

See how it's easy to manage your Angular application's state using NGXS and learn how you can use NGXS with Auth0's SDK to handle user-related functionalities.

April 29, 2022

Angular

State Management in Angular Using NGXS - Part 2

See how it's easy to manage your Angular application's state using NGXS and learn how you can use NGXS with Auth0's SDK to handle user-related functionalities.

April 29, 2022

TL;DR: In the first part of the state management in Angular with NGXS series, we learned about the basics of NGXS, how it works, and how to use it to manage our application's state. In part 2, I will show you how to use Auth0 with NGXS to manage use-related states.


User State Management

The user store will work similarly to how the menus state management works. I won't detail how each part of the state works and will focus more on the Auth0 and NGXS integration.

The starter app uses the Auth0 SDK directly and manages the roles through the RolesService. This tutorial section will walk you through migrating to using Auth0's SDK through NGXS and managing the roles by using the NGXS's Select function.

Create user model

Let's start with defining the interface for the user state object. Open user.model.ts and add the following code πŸ‘‡

// src/app/core/state/user/user.model.ts

import { User as Auth0User } from "@auth0/auth0-spa-js";

export interface UserStateModel {
  userDetails: Auth0User | undefined;
}

Create a barrel export for the user directory with the following code πŸ‘‡

// src/app/core/state/user/index.ts

export * from "./user.model";

To further simplify our imports, add the user folder to the barrel export in the state folder and add the following code πŸ‘‡

// src/app/core/state/index.ts

export * from "./menus";

// ✨ New πŸ‘‡
export * from "./user";

Create user actions

We have three user-related Actions that we need for our user Store. Login, logout, and user changed Action to keep the user details in our Store in sync with Auth0's SDK.

Since we have several user Actions that will originate from the Navbar component, you can group them under AllNavbarAction to ensure you aren't reusing these Actions in a different part of the application (following the Good Action Hygiene pattern).

Because the UserChangedFromAuth0SDK Action originates from Auth0's SDK, let's name the source part of the Action type as Auth0 SDK.

Open user.actions.ts and add the following code πŸ‘‡

// src/app/core/state/user/user.actions.ts

import { User as Auth0User } from "@auth0/auth0-spa-js";

export namespace User {
  export namespace AllNavbarActions {
    export class LoginFlowInitiated {
      static readonly type = "[Navbar] Login Flow Initiated";
    }

    export class LogoutFlowInitiated {
      static readonly type = "[Navbar] Logout Flow Initiated";
    }
  }
  export class UserChangedFromAuth0SDK {
    static readonly type = "[Auth0 SDK] User Changed";
    constructor(public payload: { user: Auth0User | undefined }) {}
  }
}

Add user.actions to the barrel export. Open index.ts and add the following code πŸ‘‡

// src/app/core/state/user/index.ts

export * from "./user.model";

// ✨ New πŸ‘‡
export * from "./user.actions";

Update application to use user actions

Like what you did with menus-related functionalities in the first part of the article, let's update the application's user-related functionalities to use NGXS's Actions. You can use Actions by injecting the Store class and calling the dispatch function with the Action name you defined in the previous section.

Open nav-bar.component.ts and add the following code πŸ‘‡

// src/app/shared/components/nav-bar/nav-bar.component.ts

import { Component } from "@angular/core";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faHome, faUser, faUtensils } from "@fortawesome/free-solid-svg-icons";
import { AuthService } from "@auth0/auth0-angular";

// ✨ New πŸ‘‡
import { Store } from "@ngxs/store";
import { User } from "src/app/core";

export interface INavBarMenuLinkProps {
  to: string;
  icon: IconDefinition;
  label: string;
}

@Component({
  selector: "app-nav-bar",
  templateUrl: "./nav-bar.component.html",
  styleUrls: ["./nav-bar.component.scss"],
})
export class NavBarComponent {
  faUser = faUser;
  isAuthenticated$ = this.authService.isAuthenticated$;
  user$ = this.authService.user$;

  navOptions: INavBarMenuLinkProps[] = [
    { to: "/home", label: "Home", icon: faHome },
    { to: "/menu", label: "Menu", icon: faUtensils },
  ];

  constructor(
    private authService: AuthService,
    // ✨ New πŸ‘‡
    private store: Store
  ) {}

  loginWithRedirect(): void {
    // ✨ New πŸ‘‡
    this.store.dispatch(new User.AllNavbarActions.LoginFlowInitiated());
  }

  logout(): void {
    // ✨ New πŸ‘‡
    this.store.dispatch(new User.AllNavbarActions.LogoutFlowInitiated());
  }
}

Create user state

Before creating individual selectors for slices of the user state, let's start by creating the boilerplate required to use this feature. NGXS uses an Injectable class with an additional State decorator. Create user.state.ts and add the following code πŸ‘‡

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";
import { State } from "@ngxs/store";
import { UserStateModel } from "./user.model";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {}

Next, Let's add some utility Selectors for user, isLoggedIn, userRoles, and isAdmin to easily let your components access these properties. Open user.state.ts and update it with the following code πŸ‘‡

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";
import { UserStateModel } from "./user.model";

// ✨ New πŸ‘‡
import { State, Selector } from "@ngxs/store";

// ✨ New πŸ‘‡
import { environment } from "src/environments/environment";

// ✨ New πŸ‘‡
import { USER_ROLES } from "../../services";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {
  // ✨ New πŸ‘‡
  @Selector()
  static user(state: UserStateModel) {
    return state.userDetails;
  }

  // ✨ New πŸ‘‡
  @Selector()
  static isLoggedIn(state: UserStateModel) {
    return !!state.userDetails;
  }

  // ✨ New πŸ‘‡
  @Selector()
  static userRoles(state: UserStateModel) {
    return (
      state.userDetails?.[`${environment.auth.audience}/roles`] || undefined
    );
  }

  // ✨ New πŸ‘‡
  @Selector()
  static isAdmin(state: UserStateModel) {
    return state.userDetails?.[`${environment.auth.audience}/roles`]?.includes(
      USER_ROLES.MENU_ADMIN
    );
  }
}

Add user.state to the list of exports in index.ts πŸ‘‡

// src/app/core/state/user/index.ts

export * from "./user.model";
export * from "./user.actions";

// ✨ New πŸ‘‡
export * from "./user.state";

Update application to use user selectors

The user role route guard previously used the value of the roles from RolesService. Since you now have a user roles Selector, you can replace the current implementation with a Selector created in the previous section. Open user-role.guard.ts and add the following codeπŸ‘‡

// src/app/core/guards/user-role.guard.ts

import { Injectable } from "@angular/core";
import {
  CanActivate,
  Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
} from "@angular/router";
import { Observable, of } from "rxjs";
import { catchError, map } from "rxjs/operators";

// ✨ New πŸ‘‡
import { Store } from "@ngxs/store";
import { UserState } from "..";

@Injectable({ providedIn: "root" })
export class UserRoleGuard implements CanActivate {
  constructor(
    private router: Router,
    // ✨ New πŸ‘‡
    private store: Store
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> {
    // ✨ Update this πŸ‘‡
    return this.store.select(UserState.userRoles).pipe(
      map((roles) => {
        if (roles && roles.includes(route?.data?.role)) {
          return true;
        }

        // redirect the user to home
        this.router.navigate(["/home"]);
        return false;
      }),
      catchError((err) => {
        // redirect the user to home
        this.router.navigate(["/home"]);
        return of(false);
      })
    );
  }
}

Another place we use user-related information is the Navbar. We display user information and conditionally display the login and logout button depending on the user's isAuthenticated state. Open nav-bar.component.ts and add the following code πŸ‘‡

// src/app/shared/components/nav-bar/nav-bar.component.ts

import { Component } from "@angular/core";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faHome, faUser, faUtensils } from "@fortawesome/free-solid-svg-icons";
import { Store } from "@ngxs/store";

// ✨ New πŸ‘‡
import { User, UserState } from "src/app/core";

export interface INavBarMenuLinkProps {
  to: string;
  icon: IconDefinition;
  label: string;
}

@Component({
  selector: "app-nav-bar",
  templateUrl: "./nav-bar.component.html",
  styleUrls: ["./nav-bar.component.scss"],
})
export class NavBarComponent {
  faUser = faUser;

  // ✨ New πŸ‘‡
  isAuthenticated$ = this.store.select(UserState.isLoggedIn);

  // ✨ New πŸ‘‡
  user$ = this.store.select(UserState.user);

  navOptions: INavBarMenuLinkProps[] = [
    { to: "/home", label: "Home", icon: faHome },
    { to: "/menu", label: "Menu", icon: faUtensils },
  ];

  constructor(private store: Store) {}

  loginWithRedirect(): void {
    this.store.dispatch(new User.AllNavbarActions.LoginFlowInitiated());
  }

  logout(): void {
    this.store.dispatch(new User.AllNavbarActions.LogoutFlowInitiated());
  }
}

The profile page displays some information about the user, such as name and photo. Since this is stored as part of the user Store, let's also update this to use the user Selector. Open profile.component.ts and add the following code πŸ‘‡

// src/app/features/profile/profile.component.ts

import { Component } from "@angular/core";

// ✨ New πŸ‘‡
import { Store } from "@ngxs/store";

// ✨ New πŸ‘‡
import { UserState } from "src/app/core";

@Component({
  selector: "app-profile",
  templateUrl: "./profile.component.html",
  styleUrls: ["./profile.component.scss"],
})
export class ProfileComponent {
  // ✨ New πŸ‘‡
  user$ = this.store.select(UserState.user);

  // ✨ New πŸ‘‡
  constructor(private store: Store) {}
}

We display an Add button when the logged-in user is an admin user on the Menu Items page. You can read more about setting up admin users on Auth0 in this blog post. Open menu-items.component.ts and add the following code πŸ‘‡

// src/app/features/menu/menu-items/menu-items.component.ts

import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Store } from "@ngxs/store";

// ✨ New πŸ‘‡
import { MenusState, UserState } from "src/app/core";

@Component({
  selector: "app-menu-items",
  templateUrl: "./menu-items.component.html",
  styles: [
    `
      :host {
        width: 100%;
        height: 100%;
      }
    `,
  ],
})
export class MenuItemsComponent {
  menuItems$ = this.store.select(MenusState.menuItems);

  // ✨ New πŸ‘‡
  isAdmin$ = this.store.select(UserState.isAdmin);

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private store: Store
  ) {}

  addMenuItem(): void {
    this.router.navigate(["add"], { relativeTo: this.activatedRoute });
  }
}

Lastly, we display the Edit and Delete buttons when the logged-in user is an admin on the Menu Item page. Open menu-item.component.ts and add the following code πŸ‘‡

// src/app/features/menu/menu-item/menu-item.component.ts

import { Component } from "@angular/core";
import { Location } from "@angular/common";
import { ActivatedRoute, Router } from "@angular/router";
import { map, switchMap } from "rxjs/operators";
import { Store } from "@ngxs/store";

// ✨ New πŸ‘‡
import { MenusState, UserState } from "src/app/core";

@Component({
  selector: "app-menu-item",
  templateUrl: "./menu-item.component.html",
  styleUrls: ["./menu-item.component.scss"],
})
export class MenuItemComponent {
  menuItemId$ = this.activatedRoute.params.pipe(map((params) => params.id));

  menuItem$ = this.menuItemId$.pipe(
    switchMap((id) => this.store.select(MenusState.menuItem(id)))
  );

  // ✨ New πŸ‘‡
  isAdmin$ = this.store.select(UserState.isAdmin);

  constructor(
    private activatedRoute: ActivatedRoute,
    private router: Router,
    private location: Location,
    private store: Store
  ) {}

  back(): void {
    this.location.back();
  }

  navigateTo(url: string): void {
    this.router.navigateByUrl(`${this.router.url}/${url}`);
  }
}

Create user action handlers

Like the Menus action handlers, the User state will use the Action decorator to handle user-related actions. For the User state, the action handlers will be where the app interact with Auth0's SDK - logging in, logging out, and updating the state with Auth0's user object.

Let's start with the login and logout action handlers that trigger their respective flows in Auth0's SDK. Open user.state.ts and add the following code πŸ‘‡

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";

// ✨ New πŸ‘‡
import { State, Action, StateContext, Store, Selector } from "@ngxs/store";
// ✨ New πŸ‘‡
import { AuthService } from "@auth0/auth0-angular";

import { environment } from "src/environments/environment";
import { UserStateModel } from "./user.model";
import { User } from "./user.actions";
import { USER_ROLES } from "../../services";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {
  constructor(
    private store: Store,
    // ✨ New πŸ‘‡
    private authService: AuthService
  ) {}

  // ✨ New πŸ‘‡
  @Action(User.AllNavbarActions.LoginFlowInitiated)
  login() {
    this.authService.loginWithRedirect();
  }

  // ✨ New πŸ‘‡
  @Action(User.AllNavbarActions.LogoutFlowInitiated)
  logout() {
    this.authService.logout();
  }

  @Selector()
  static user(state: UserStateModel) {
    return state.userDetails;
  }

  @Selector()
  static isLoggedIn(state: UserStateModel) {
    return !!state.userDetails;
  }

  @Selector()
  static userRoles(state: UserStateModel) {
    return (
      state.userDetails?.[`${environment.auth.audience}/roles`] || undefined
    );
  }

  @Selector()
  static isAdmin(state: UserStateModel) {
    return state.userDetails?.[`${environment.auth.audience}/roles`]?.includes(
      USER_ROLES.MENU_ADMIN
    );
  }
}

To keep the user observable from the Auth0's SDK and our User State in sync, create a subscription to listen to AuthService's user$ property and dispatch the UserChangedFromAuth0SDK whenever the observable emits a new value. UserChangedFromAuth0SDK will then update the User State with the new user data from Auth0. Open user.state.ts and add the following code πŸ‘‡

// src/app/core/state/user/user.state.ts

import { Injectable } from "@angular/core";
import { State, Action, StateContext, Store, Selector } from "@ngxs/store";
import { AuthService } from "@auth0/auth0-angular";
import { environment } from "src/environments/environment";
import { USER_ROLES } from "../../services";
import { UserStateModel } from "./user.model";
import { User } from "./user.actions";

@State<UserStateModel>({
  name: "user",
  defaults: {
    userDetails: undefined,
  },
})
@Injectable()
export class UserState {
  constructor(private store: Store, private authService: AuthService) {
    // ✨ New πŸ‘‡
    this.listenToUserChange();
  }

  // ✨ New πŸ‘‡
  @Action(User.UserChangedFromAuth0SDK)
  userChangedFromAuth0SDK(
    ctx: StateContext<UserStateModel>,
    actions: User.UserChangedFromAuth0SDK
  ) {
    const state = ctx.getState();
    ctx.setState({
      ...state,
      userDetails: actions.payload.user,
    });
  }

  @Action(User.AllNavbarActions.LoginFlowInitiated)
  login() {
    this.authService.loginWithRedirect();
  }

  @Action(User.AllNavbarActions.LogoutFlowInitiated)
  logout() {
    this.authService.logout();
  }

  // ✨ New πŸ‘‡
  private listenToUserChange(): void {
    this.authService.user$.subscribe((user) => {
      this.store.dispatch(
        new User.UserChangedFromAuth0SDK({ user: user || undefined })
      );
    });
  }

  @Selector()
  static user(state: UserStateModel) {
    return state.userDetails;
  }

  @Selector()
  static isLoggedIn(state: UserStateModel) {
    return !!state.userDetails;
  }

  @Selector()
  static userRoles(state: UserStateModel) {
    return (
      state.userDetails?.[`${environment.auth.audience}/roles`] || undefined
    );
  }

  @Selector()
  static isAdmin(state: UserStateModel) {
    return state.userDetails?.[`${environment.auth.audience}/roles`]?.includes(
      USER_ROLES.MENU_ADMIN
    );
  }
}

Update app module

You will then need to add UserState to the NgxsModule initialization. Open app.module.ts and add the following code.

// src/app/app.module.ts

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AuthHttpInterceptor, AuthModule } from "@auth0/auth0-angular";

import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { NavBarModule } from "./shared";
import { environment } from "src/environments/environment";
import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { NgxsModule } from "@ngxs/store";
import { NgxsReduxDevtoolsPluginModule } from "@ngxs/devtools-plugin";

// ✨ Update this πŸ‘‡
import { MenusState, UserState } from "./core";

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    AuthModule.forRoot({
      ...environment.auth,
      cacheLocation: "localstorage",
      httpInterceptor: {
        allowedList: [
          `${environment.serverUrl}/api/menu/items`,
          `${environment.serverUrl}/api/menu/items/*`,
        ],
      },
    }),
    AppRoutingModule,
    NavBarModule,
    // ✨ Update this πŸ‘‡
    NgxsModule.forRoot([MenusState, UserState], { developmentMode: true }),
    NgxsReduxDevtoolsPluginModule.forRoot(),
  ],
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHttpInterceptor,
      multi: true,
    },
  ],
})
export class AppModule {}

Checkpoint: There aren't any visible functional changes added in this section. The difference between the app's current state and the previous checkpoint is its underlying implementation. The app now uses NGXS versus a BehaviorSubject to manage user-related states. The app should display the login button with an empty dashboard when the user is not authenticated and display the logout button with the logged-in user's name along with the menu items on the dashboard when a user is authenticated. Clicking on the 'Log in' and 'Log out' buttons should trigger their respective flows using Auth0's SDK and update your application's state. If you open Redux Devtools in your browser, you should see the user state and actions every time you perform any user-related actions.

Conclusion

State management is a key component when building applications. You added two Stores to our demo application to manage two distinct states - menus and users. This is a relatively small demo application with a few Stores, Actions, and Selectors to show how you can use NGXS to manage your Application's state and use NGXS with Auth0's SDK to handle user-related functionalities.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon