Search code examples
angularangular-routingngrxngrx-storeangular-canload

canLoad in children routes using NgRx


I'm working in an Angular 9 app and I'm trying to load (or not) an specific module only when my app state (ngrx) have a property != null

First, I have an AuthGuard in my routes but with canActivate. So I want the 'dashboard' module only loads when mt AppState have a token

Here is my route file

const routes: Routes = [
{
  path: '',
  component: AppLayoutComponent,
  canActivate: [ AuthGuard ],
  children: [
    { path: '',  loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule) }
  ]
},
{
  path: '',
  component: AuthLayoutComponent,
  children: [
    { path: 'session',  loadChildren: () => import('./pages/modules/session/session.module').then(m => m.SessionModule) }
  ]
},
{
  path: '**',
  redirectTo: 'session/not-found'
}];

And this is my AuthGuard. It localestorage doesn't have a session, then redirects to the login page.

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private router: Router, public authService: AuthService) {}


  public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (localStorage.getItem('session')) {
        // logged in so return true
        return true;
    }

    // not logged in so redirect to login page with the return url
    this.router.navigate(['session/signin']);
    return false;
  }
}

this is kind of what I want to do whit the canLoad in AuthModuleGuard, but this doesn't work

  public canLoad(): Observable<boolean> {
    return this.store.select('session').pipe(
      take(1),
      map((authstate) => {
          console.log('Token status', authstate.token);
          if (authstate.token !== null) {
              return true;
          } else {
              this.router.navigate(['session/signin']);
              return false;
          }
        })
      );
  }

if I do this... the application gives me errors and still loads both files

{
  path: '',
  component: AppLayoutComponent,
  canLoad: [ AuthModuleGuard ],
  children: [ ... ]
}

enter image description here

and If I do this ... the app never finish loading

{ path: '', canLoad: [ AuthModuleGuard ], loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule) },

HERE IS A STACKBLITZ EXAMPLE (including my folder structure)---> https://stackblitz.com/edit/angular-ivy-nasx7r

I need a way to load the dashboard module (and other modules) only if the token in my store are seted, and If is not, redirect to the Login. Please help


Solution

  • After spending some time on this, I've learnt some very interesting things:

    if (route.children) {
      // The children belong to the same module
      return of(new LoadedRouterConfig(route.children, ngModule));
    }
    
      if (route.loadChildren) { /* ... */ }
    

    This also denotes that canLoad is redundant in this case:

    {
      path: '',
      component: AppLayoutComponent,
      canLoad: [ AuthModuleGuard ],
      children: [ ... ]
    }
    

    as this route guard has effect when used together with loadChildren.

    • you should be mindful of when to redirect from your guard

      With a configuration like this:

    {
      path: '',
      component: AppLayoutComponent,
      children: [
        { 
          path: '', 
          loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule),
          canLoad: [AuthModuleGuard]
        }
      ]
    },
    

    and a canLoad guard like this:

    canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> {
      return this.store.select('session').pipe(
          take(1),
          map((authstate) => {
              console.log('Token status', authstate.token);
              if (authstate.token !== null) {
                  return true;
              } else {
                  this.router.navigate(['session/signin']);
                  return false;
              }
          })
      );
    }
    

    you'll get into an infinite loop. When the app first loads, it will go through each configuration in a depth-first manner and will compare the path with the current segments(initially, segments = []).

    But remember that if a route has a children property, it will go through each of them and see if the segments match the route. Since the child route has path: '', it will match any segments and because it has loadChildren, it will invoke the canLoad guards.

    Eventually, this lines will be reached:

    this.router.navigate(['session/signin']);
    return false;
    

    this.router.navigate(['session/signin']); indicates a redirect, which means it will repeat the steps delineated above.


    The solution I came up with is to add a pathMatch: 'full' to your child route:

    {
      path: '',
      component: AppLayoutComponent,
      children: [
        { 
          path: '', 
          pathMatch: 'full',
          loadChildren: () => import('./pages/modules/dashboard/dashboard.module').then(m => m.DashboardModule),
          canLoad: [AuthModuleGuard]
        }
      ]
    },
    

    When the app loads, the segments will be an empty array and because path: '' matches any group of segments and that group of segments is [] initially, there will be a match:

    if (route.path === '') {
      if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) {
        return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
      }
    
      return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
    }
    

    This means that the guard will be invoked and the if's alternative block will be reached and this.router.navigate(['session/signin']) will be invoked.

    Next time the comparison is made, the segments will be (roughly) ['session', 'signin'] and there will be no match, since this is returned:

    {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}}
    

    If no match occurs, it will keep on searching until something is found, but the guard will not be invoked again.

    StackBlitz