Angular Feature Module And Lazy Loading Angular

Aug 16th, 2019 - written by Kimserey with .

Angular offers a way to separate an application into modules. There are many ways to group functionalities into modules and the decision is left to the programmer to find the best composition for the application. For example, a module can group a part of the application domain logic, or it can also serve as a grouping for reusable directives, or a grouping for providers. I have describe briefly about two years ago what a module is in Angular looking into the details of the NgModule decorator. Today I will dive into more details on how to separate an application into feature modules and what doing so provides us.

Root module

Every application defines at least one module, the root module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

From the root module, usually called AppModule, we import other modules. Those modules might be coming from Angular libraries like BrowserModule or HttpClientModule or coming external libraries like PrimeNG.

Each one of the modules imported provide different sort of functionality. BrowserModule provides directive to run Angular application on the browser while also exporting the CommonModule allowing us to use common Angular directive like ngIf.

BrowserModule, just like AppModule, is an Angular module. We can see its definition from Angular codebase:

1
2
3
4
@NgModule({providers: BROWSER_MODULE_PROVIDERS, exports: [CommonModule, ApplicationModule]})
export class BrowserModule {
    // skip content of the module
}

And CommonModule:

1
2
3
4
5
6
7
8
@NgModule({
  declarations: [COMMON_DIRECTIVES, COMMON_PIPES],
  exports: [COMMON_DIRECTIVES, COMMON_PIPES],
  providers: [
    {provide: NgLocalization, useClass: NgLocaleLocalization},
  ],
})
export class CommonModule { }

Where the declaration of common directives and common pipes resides. Following that, we can see the list of directives and the list of pipes available:

1
2
import {COMMON_DIRECTIVES} from './directives/index';
import {COMMON_PIPES} from './pipes/index';
1
2
3
4
5
6
7
8
9
10
11
12
13
export const COMMON_DIRECTIVES: Provider[] = [
  NgClass,
  NgComponentOutlet,
  NgForOf,
  NgIf,
  NgTemplateOutlet,
  NgStyle,
  NgSwitch,
  NgSwitchCase,
  NgSwitchDefault,
  NgPlural,
  NgPluralCase,
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const COMMON_PIPES = [
  AsyncPipe,
  UpperCasePipe,
  LowerCasePipe,
  JsonPipe,
  SlicePipe,
  DecimalPipe,
  PercentPipe,
  TitleCasePipe,
  CurrencyPipe,
  DatePipe,
  I18nPluralPipe,
  I18nSelectPipe,
  KeyValuePipe,
];

Importing CommonModule gives us access to the build in Angular directives and pipes. Other libraries like PrimeNG define reusable components. Those components might be buttons or cards or even calendars. Importing those components is similar to importing directives:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ButtonModule } from 'primeng/button';
// other imports omitted

@NgModule({
  imports: [
    BrowserModule,
    ButtonModule
  ],
  declarations: [
    AppComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Lastly other modules like HttpClientModule provide reusable services like HttpClient which is registered as a similar way as above.

So what we have seen so far is that a module can be used to group:

  • Directives
  • Pipes
  • Components
  • Providers

They can be group in logical reusable widget element like ButtonModule from PrimeNG, or they can be used to group utilities that are frequently necessary like CommonModule, or they can be used to setup reusable services like HttpClient. This flexibility in grouping also allows us to introduce another type of module known as feature modules.

Feature module

A feature module, or domain module, can be used to separate a part of functionality in an application. For example we might have an admin portion of the application which has completely different components and logic.

1
2
3
4
5
6
7
8
9
app/
  admin/
    components/
    admin.module.ts
  shop/
    components/
    shop.module.ts
  app.component.ts
  app.module.ts

Taking for example a simple application being an online shop, we might identify a shop module and a admin module. The result of this structure is three modules:

  • admin.module
  • shop.module
  • app.module

And the AppModule would then import:

1
2
3
4
5
6
7
8
9
10
11
12
13
@NgModule({
  imports: [
    BrowserModule,
    AdminModule,
    ShopModule
  ],
  declarations: [
    AppComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

This would allow the AppComponent to use components from either ShopModule or AdminModule. Another way to provide an even more pronounced separation is to use routes.

RouterModule provides a way to separate modules into specific child routes. Using it we will be able to provide a complete separation based on routes between shop and admin. Any calls to a subroutes of /shop will result in a route defined in shop while any subroutes of /admin will result in a route defined in admin.

In order to setup routes, we import the RouterModule and provide routes to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export const routes: Routes = [
  { path: '', redirectTo: '/shop', pathMatch: 'full' },
  { path: 'shop', component: ShopComponent },
  { path: 'admin', component: AdminComponent }
];

@NgModule({
  imports: [
    BrowserModule,
    AdminModule,
    ShopModule,
    RouterModule.forRoot(routes)
  ],
  declarations: [
    AppComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

And in our AppComponent, we use router-outlet to specify where the content of the route will go.

1
<router-outlet></router-outlet>

Another way advocated by the Angular tutorial is to create a specific module dedicated for routing. This will allow us to keep our root module clean and also have a dedicated place to handle routing.

1
2
3
4
5
6
7
8
9
10
11
12
export const routes: Routes = [
  { path: '', redirectTo: '/shop', pathMatch: 'full' },
  { path: 'shop', component: ShopComponent },
  { path: 'admin', component: AdminComponent }
];


@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

And we can then import this routing module in our AppModule:

1
2
3
4
5
6
7
8
9
10
11
12
13
@NgModule({
  imports: [
    BrowserModule,
    AdminModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

As we can see from the AppRoutingModule, specific routes point to the components imported from the different modules. As the application grows, we will have more routes under shop and similarly more routes under admin. In order to keep the routing map maintainable, we can create a dedicated routing module in each modules.

1
2
3
4
5
6
7
8
9
10
11
12
app/
  admin/
    components/
    admin-routing.module.ts
    admin.module.ts
  shop/
    components/
    shop-routing.module.ts
    shop.module.ts
  app.component.ts
  app-routing.module.ts
  app.module.ts

Within the feature routing module, we will be moving the route:

1
2
3
4
5
6
7
8
9
export const routes: Routes = [
  { path: 'admin', component: AdminComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AdminRoutingModule { }

We also do the similar changes for Shop and then within the root routes, we can remove the extracted routes:

1
2
3
4
5
6
7
8
9
export const routes: Routes = [
  { path: '', redirectTo: '/shop', pathMatch: 'full' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

There is a slight difference in the import statement for root module versus feature modules, for root we use the static function RouterModule.forRoot(routes) to register our routes versus RouterModule.forChild(routes) to register our routes in feature module. This difference comes from the fact that forRoot registers providers, on top of registering routes, that have to be registered only once. Therefore for feature modules, we use .forChild(routes) which will only register the routes. This pattern forRoot and forChild is used in many libraries to provide shared functionalities which needs to be applied across the whole application. Both forRoot and forChild functions return a ModuleWithProviders which allow the user of the shared module to provide parameters to instantiate the providers, like how we provide routes to the RouterModule. More details can be found in the implementation of the router.

We now have an application broken into two feature modules with routing module for each of them, ready to grow in term of functionality. The last point we will cover is lazy loading of feature module.

Lazy Loading Module

So far our module have been imported in the AppModule. This import is known as eager loading. The advantage of having modules define this way is that we can make then load lazily. Lazy loading allows us to shrink the size of the application as only necessary modules will be loaded by the browser. In our case it is beneficial as it is unlikely that a user will need the admin portion of the site, and respectively it is unlikely that an admin will need the user portion of the site. Therefore we can safely reduce the size of the application by introducing lazy loading.

Lazy loading is made possible by the RouterModule. In order to do that, we remove the import from the AppModule:

1
2
3
4
5
6
7
8
9
10
11
@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Then in the root routes, we use loadChildren to introduce lazy loading using import function for typesafety.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: '/shop', pathMatch: 'full' },
  {
    path: 'shop',
    loadChildren:  () => import('./shop/shop.module').then(m => m.ShopModule)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

LoadChildren is a function which will be called only when we need to access the route, only then, the module will be loaded. We could also use the string format which isn’t typesafe loadChildren: './shop/shop.module#ShopModule'. Since we have added a feature path already in the root routing directory, we can remove the path on the feature routing modules:

1
2
3
export const routes: Routes = [
  { path: '', component: ShopComponent }
];

The route will be prefixed by /shop coming from the root routing. And that concludes today post!

Conclusion

Today we saw how we could make use of Angular modules to separate the domain logic of our application. We started by looking at how we were already using modules from Angular library and how they were defined. We then moved on to define our own feature modules and we completed the post by making those module lazy. I hope you liked this post and I see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.