Angular

State Management in Angular Using Akita - Pt. 2

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

Last Updated On: February 10, 2022

Angular

State Management in Angular Using Akita - Pt. 2

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

Last Updated On: February 10, 2022

State Management with Akita - Part 2

TL;DR: In the first part of the state management in Angular with Akita series, we learned about the basics of Akita, 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 Akita 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 focus more on the Auth0 and Akita integration.

The starter app is using the Auth0 SDK directly and managing the roles through the RolesService, this section of the tutorial will walk you through migrating to using Auth0's SDK through Akita and managing the roles by using Queries.

Create user store

Let's start with defining the interface for the user state object and its initial state. Open user.store.ts and add the following code 👇

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

import { Injectable } from "@angular/core";
import { Store, StoreConfig } from "@datorama/akita";
import { User as Auth0User } from "@auth0/auth0-spa-js";

export interface UserState {
  user: Auth0User | undefined;
}

export function createInitialState(): UserState {
  return {
    user: undefined,
  };
}
@StoreConfig({ name: "user" })
@Injectable({ providedIn: "root" })
export class UserStore extends Store<UserState> {
  constructor() {
    super(createInitialState());
  }
}

Create a barrel export for the user directory with the following code 👇

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

export * from "./user.store";

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 { createAction, props } from "@datorama/akita-ng-effects";
import { User as Auth0User } from "@auth0/auth0-spa-js";

export const allNavbarActions = {
  loginFlowInitiated: createAction("[Navbar] Login Flow Initiated"),
  logoutFlowInitiated: createAction("[Navbar] Logout Flow Initiated"),
};

export const userChangedFromAuth0SDK = createAction(
  "[Auth0 SDK] User Changed",
  props<{ 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.store";

// ✨ New 👇
export * from "./user.actions";

Update application to use user actions

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 { Actions } from "@datorama/akita-ng-effects";

// ✨ New 👇
import { allNavbarActions } from "src/app/core/state/user";

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 actions: Actions
  ) {}

  loginWithRedirect(): void {
    // ✨ New 👇
    this.actions.dispatch(allNavbarActions.loginFlowInitiated());
  }

  logout(): void {
    // ✨ New 👇
    this.actions.dispatch(allNavbarActions.logoutFlowInitiated());
  }
}

Create user query

Before creating individual queries for slices of the user state, let's start by creating the boilerplate required to use this feature. Akita uses an Injectable class that extends Akita's Query class. Create user.query.ts and add the following code 👇

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

import { Injectable } from "@angular/core";
import { Query } from "@datorama/akita";
import { UserState } from "./user.store";

@Injectable({ providedIn: "root" })
export class UserQuery extends Query<UserState> {
  selectUser$ = this.select("user");
}

Next, Let's add some utility Queries for isLoggedIn, userRoles, and isAdmin to let your components access these properties easily. Open user.query.ts and update it with the following code 👇

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

import { Injectable } from "@angular/core";
import { Query } from "@datorama/akita";

// ✨ New 👇
import { map } from "rxjs/operators";

// ✨ New 👇
import { environment } from "src/environments/environment";

// ✨ New 👇
import { UserStore, UserState } from "./user.store";

// ✨ New 👇
export const USER_ROLES = {
  MENU_ADMIN: "menu-admin",
};
@Injectable({ providedIn: "root" })
export class UserQuery extends Query<UserState> {
  selectUser$ = this.select("user");

  // ✨ New 👇
  selectIsLoggedIn$ = this.selectUser$.pipe(map((user) => !!user));

  // ✨ New 👇
  selectUserRoles$ = this.selectUser$.pipe(
    map((user) => user?.[`${environment.auth.audience}/roles`] || undefined)
  );

  // ✨ New 👇
  selectIsAdmin$ = this.selectUserRoles$.pipe(
    map((userRoles) => userRoles?.includes(USER_ROLES.MENU_ADMIN))
  );

  constructor(protected store: UserStore) {
    super(store);
  }
}

Add user.query to the list of exports in index.ts 👇

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

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

// ✨ New 👇
export * from "./user.query";

Update application to use user queries

The user role route guard was previously using the value of the roles from RolesService. Since you now have a user roles Query, you can replace the current implementation with a query 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 { UserQuery } from "../state/user";

@Injectable({ providedIn: "root" })
export class UserRoleGuard implements CanActivate {
  constructor(
    private router: Router,
    // ✨ New 👇
    private userQuery: UserQuery
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> {
    // ✨ Update this 👇
    return this.userQuery.selectUserRoles$.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 { Actions } from "@datorama/akita-ng-effects";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faHome, faUser, faUtensils } from "@fortawesome/free-solid-svg-icons";

// ✨ New 👇
import { allNavbarActions, UserQuery } from "src/app/core/state/user";

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.userQuery.selectIsLoggedIn$;

  // ✨ New 👇
  user$ = this.userQuery.selectUser$;

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

  // ✨ New 👇
  constructor(private userQuery: UserQuery, private actions: Actions) {}

  loginWithRedirect(): void {
    this.actions.dispatch(allNavbarActions.loginFlowInitiated());
  }

  logout(): void {
    this.actions.dispatch(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 selectUser$ Query. Open profile.component.ts and add the following code 👇

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

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

// ✨ New 👇
import { UserQuery } from "src/app/core/state/user";

@Component({
  selector: "app-profile",
  templateUrl: "./profile.component.html",
  styleUrls: ["./profile.component.scss"],
})
export class ProfileComponent {
  // ✨ New 👇
  user$ = this.userQuery.selectUser$;

  // ✨ New 👇
  constructor(private userQuery: UserQuery) {}
}

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 { MenusQuery } from "src/app/core/state/menus";

// ✨ New 👇
import { UserQuery } from "src/app/core/state/user";

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

  // ✨ New 👇
  isAdmin$ = this.userQuery.selectIsAdmin$;

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    // ✨ New 👇
    private userQuery: UserQuery,
    private menuQuery: MenusQuery
  ) {}

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

Lastly, we display a Edit and Delete buttons when the logged-in user is an admin user 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 { MenusQuery } from "src/app/core/state/menus";

// ✨ New 👇
import { UserQuery } from "src/app/core/state/user";

@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.menusQuery.selectMenuItem(id))
  );

  // ✨ New 👇
  isAdmin$ = this.userQuery.selectIsAdmin$;

  constructor(
    private activatedRoute: ActivatedRoute,
    private router: Router,
    private location: Location,
    // ✨ New 👇
    private userQuery: UserQuery,
    private menusQuery: MenusQuery
  ) {}

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

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

Create user effects

As explained at the beginning of part 1 of the "State Management with Akita" series, Effects are where side effects of the Action are handled. For the user store, this will be where the app will interact with Auth0's SDK, updating the store when the user object changes and logging in and logging out the user. Let's start with creating an empty class with the Injectable decorator. Create user.effects.ts in the core/state/user/ directory and add the following code 👇

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

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

@Injectable()
export class UserEffects {}

Next, let's add login and logout action handlers that trigger their respective flows in Auth0's SDK. Open user.effects.ts and add the following code 👇

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

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

// ✨ New 👇
import { map, tap } from "rxjs/operators";
import { AuthService } from "@auth0/auth0-angular";
import { Actions, createEffect, ofType } from "@datorama/akita-ng-effects";
import * as UserActions from "./user.actions";
import { UserStore } from "./user.store";

@Injectable()
export class UserEffects {
  // ✨ New 👇
  constructor(
    private actions$: Actions,
    private userStore: UserStore,
    private authService: AuthService
  ) {}

  // ✨ New 👇
  login$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(UserActions.allNavbarActions.loginFlowInitiated),
        tap(() => this.authService.loginWithRedirect())
      ),
    { dispatch: false }
  );

  // ✨ New 👇
  logout$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(UserActions.allNavbarActions.logoutFlowInitiated),
        tap(() => this.authService.logout())
      ),
    { dispatch: false }
  );
}

To keep the user observable from the Auth0's SDK and our user Store in sync, you can use an actionless effect that listens to an external observable (in our case authService.user$) and dispatches another action (UserActions.userChangedFromAuth0SDK) whenever that observable emits a new value. UserActions.userChangedFromAuth0SDK will then update the user store with new user data from Auth0. Open user.effects.ts and add the following code 👇

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

import { Injectable } from "@angular/core";
import { map, tap } from "rxjs/operators";
import { Actions, createEffect, ofType } from "@datorama/akita-ng-effects";
import * as UserActions from "./user.actions";
import { UserStore } from "./user.store";
import { AuthService } from "@auth0/auth0-angular";

@Injectable()
export class UserEffects {
  constructor(
    private actions$: Actions,
    private userStore: UserStore,
    private authService: AuthService
  ) {}

  // ✨ New 👇
  userChanged$ = createEffect(
    () =>
      this.authService.user$.pipe(
        map((user) =>
          UserActions.userChangedFromAuth0SDK({ user: user || undefined })
        )
      ),
    { dispatch: true }
  );

  // ✨ New 👇
  userChangedFromAuth0SDK$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(UserActions.userChangedFromAuth0SDK),
        tap((action) => {
          this.userStore.update((state) => ({
            ...state,
            user: action.user,
          }));
        })
      ),
    { dispatch: false }
  );

  login$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(UserActions.allNavbarActions.loginFlowInitiated),
        tap(() => this.authService.loginWithRedirect())
      ),
    { dispatch: false }
  );

  logout$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(UserActions.allNavbarActions.logoutFlowInitiated),
        tap(() => this.authService.logout())
      ),
    { dispatch: false }
  );
}

Add user.effects to the barrel export. Open index.ts and add the following code 👇

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

export * from "./user.store";
export * from "./user.actions";
export * from "./user.query";

// ✨ New 👇
export * from "./user.effects";

Update Effects Module

You will then need to add UserEffects to the EffectsModule 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 { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { AkitaNgEffectsModule } from "@datorama/akita-ng-effects";
import { AkitaNgDevtools } from "@datorama/akita-ngdevtools";
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 { MenusEffects } from "./core/state/menus";

// ✨ New 👇
import { UserEffects } from "./core/state/user";

@NgModule({
  imports: [
    BrowserModule,
    HttpClientModule,
    AuthModule.forRoot({
      ...environment.auth,
      httpInterceptor: {
        allowedList: [
          `${environment.serverUrl}/api/menu/items`,
          `${environment.serverUrl}/api/menu/items/*`,
        ],
      },
    }),
    AppRoutingModule,
    NavBarModule,

    // ✨ Update this 👇
    AkitaNgEffectsModule.forRoot([MenusEffects, UserEffects]),
    environment.production
      ? []
      : AkitaNgDevtools.forRoot({
          maxAge: 25,
        }),
  ],
  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 Akita 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 user. This is a relatively small demo application with a few Stores, Actions, Queries, and Effects to show how you can use Akita to manage your Application's state, and use Akita with Auth0's SDK to handle user-related functionalities.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon