Erik Slack | ng-conf | Oct 2020
Lazy-loading modules and routing in Angular is a pretty well-covered and well-rehearsed topic. But lately I’ve noticed there’s an absence of great explanations for nested lazy-loading. I’m going to go briefly over this fairly simple topic. It is really simple, but it’s not at all straightforward.
Introduction to Lazy-loaded Routes
Angular is a very elegant and opinionated framework for Typescript. There are so many useful features built into it, one of which is routing. Routing is what simulates having multiple pages in a Single Page Application. Good routing is dynamic, flexible, and configurable. Angular routing is all of those and more. One of the ways it’s configurable is that you can lazy-load your components using a combination of routing and feature modules.
Feature modules allow you to separate code in a — what else? — modular way. The benefit is that you can load chunks of code when you need them as opposed to forcing your app to load everything all at once. This can significantly improve page load times and decrease coupling of components. You might say, “my two modules are both pretty small, so I might as well eagerly load them,” but in my experience everything tends to grow over time. It’s always best to lazy-load your routes. Besides, you’re not just lazy-loading the components you built, but also the dependencies of those components. The better question is why wouldn’t you lazy-load all of your routes?
One reason you might not want to do it is because then you’ve got to add an extra module file, but trust me when I tell you that’s a super cheap price to pay. I’m not saying you have to start all routes as lazy loaded modules though; you can easily convert normal routes to lazy-loaded routes at anytime. Let me show you how.
How to Make a Lazy-loaded Route
It’s super easy! Let me show you a regular route first and then I’ll transform it into a lazy-loaded route.
const routes: Routes = [
{
path: 'home',
component: HomeComponent
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
export class AppRoutingModule {}
This route is eagerly loaded since all its dependencies are loaded in the module in which it is imported. It isn’t separating something into modules alone that makes it lazy-loaded. Here is the same route now lazy-loaded instead of eagerly loaded.
const routes: Routes = [
{
path: 'home',
loadChildren: () => import('@endless-realms/business/feature/home').then(m => m.BusinessFeatureHomeModule)
}
]
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
export class AppRoutingModule {}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { HomeComponent } from './home.component';
@NgModule({
imports: [
CommonModule,
MatButtonModule,
RouterModule.forChild([
{path: '', pathMatch: 'full', component: HomeComponent}
])
],
declarations: [HomeComponent],
})
export class GmFeatureHomeModule {}
This route is now lazy-loaded. Note that in this module we use the forChild() method instead of forRoot(). You must do this for any module that is meant to be lazy-loaded. Use forRoot() in the root module which is usually app.module.ts or the corresponding routing module.
The module, its components, and all of its dependencies will not load to the client until the user navigates to this route.
How to Nest Lazy-Loaded Routes
The previous stuff was more for beginners doing lazy-loaded routing for the first time. Now I’m going to explain the stuff that isn’t readily apparent when trying to nest routable modules.
I’ve built many applications that work better with large feature modules that contain multiple lazy-loaded child routes. So from my app.routing.ts file I lazy load these big modules with a path. Then in each large feature library, I’ll lazy-load a module for each of the components in that module. Remember that my goal is to lazy-load all routes. This requires you to nest routes.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { SignOutComponent } from './sign-out/sign-out.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: '/login'
},
{
path: 'home',
loadChildren: () => import('@endless-realms/adventure/feature/content').then(m => m.EndlessRealmsAdventureFeatureContentModule),
canActivate: [AuthGuard]
},
{
path: 'login',
loadChildren: () => import('@endless-realms/shared/feature/login').then(m => m.SharedFeatureLoginModule)
},
{
path: 'sign-out',
canActivate: [AuthGuard],
component: SignOutComponent
}
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
exports: [RouterModule]
})
export class AppRoutingModule {}
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ContentComponent } from './content.component';
const routes: Routes = [
{
path: '',
component: ContentComponent,
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule)
},
{
path: 'encounters/:encounterId',
loadChildren: () => import('./encounter/encounter.module').then(m => m.EncounterModule)
},
{
path: 'quest/:questId',
loadChildren: () => import('./quests/quests.module').then(m => m.QuestsModule)
}
]
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ContentRoutingModule { }
@NgModule({
declarations: [
QuestsComponent
],
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
pathMatch: 'full',
component: QuestsComponent,
}
])
]
})
export class QuestsModule { }
This is an example of nested lazy-loaded routes. An app loads a feature module which loads a routed module and component.
It looks really similar to the way I did it in the app.routing.ts file, but there are some key differences that aren’t readily apparent. The most important thing to keep in mind is how and when to use the pathMatch property.
Avoiding pitfalls like a boss.
Pitfalls of pathMatch
The property on a route called pathMatch is used to determine whether a route should match or not in two different ways. The default value is ‘prefix’ and this will match any route that includes it and can includes suffixes too. This is opposed to pathMatch: ‘full’ which will only match routes that end in this path. Did you have to reread that? It’s okay, it really isn’t super clear. Let me show you when to use each of these values for pathMatch.
Routes with pathMatch: ‘prefix’ (Default Value)
If you don’t specify explicitly that a route should be pathMatch: ‘full’ then it will automatically be pathMatch: ‘prefix’, which results in the following behavior:
- This route is triggered whenever a child route matches the path of this route.
- This route overrides other routes that include its path + suffix.
Routes with pathMatch: ‘full’
Specifying pathMatch: ‘full’ results in the following behavior:
- This route will only be triggered if it doesn’t have a suffix after the path.
When to Use Each
You’ll want to use pathMatch: ‘full’ in the following situations:
- When you want a route to redirect to another path using path: ‘’ aka empty route.
- When you have a route with a route param that should use a different component than the route without the param. For example:
‘/accounts/:accountId’
vs
‘/accounts’
You’ll want to use default pathMatch for every other situation.
Here’s a tip: if a route should match child routes — meaning you want to nest routes then you must have a base component for each parent route that contains a router outlet, i.e.
<router-outlet></router-outlet>
Your child routes won’t show up if you don’t have that element. If you make the misguided mistake of putting pathMatch: ‘full’ on that route then none of your child routes will work. Take it off immediately! Voila! It works now.
Hopefully, this helps save someone else a bit of confusion about nested routing.
What’s next?
Check out this article I wrote last about using Angular with Firebase and Flamelink to give you Firebase-powered CMS in your Angular application!
I also recommend that you scroll through the list of other articles published by ng-conf. Click here to read them all!
EnterpriseNG Logo
I hope I see you at EnterpriseNG!
EnterpriseNG is a two-day conference by the team that puts on ng-conf focused on Angular in the enterprise. I’ll be there — virtually of course. It’s taking place November 19th and 20th. Check it out at ng-conf.org.
All of us partying together next month at EnterpriseNG!
Thank you for reading!
If you liked this article, please give me some claps — you can give up to 50! That helps me celebrate my accomplishment of successfully teaching others how to do this thing. You can also follow me on twitter at @erik_slack. I invite you to dm me if you have any questions about my article. Thank you for reading it, please share!
ng-conf: The Musical is coming
ng-conf: The Musical is a two-day conference from the ng-conf folks coming on April 22nd & 23rd, 2021. Check it out at ng-conf.org
Top comments (0)