[UPDATE] Configuring Lazy Loaded Angular Modules

Making our Angular modules configurable is essential in building a reusable architecture. But in the recent past, Angular presented us with its new moduleless future. How does it look now? How can we configure lazy-loaded components? Let's have a look at how we provide our configurations directly.

In this article:

SL-rund
Sascha Lehmann is Developer at Thinktecture. He focusses on web-based frontends with Angular and User Experience improvement.

All examples and code references refer to Angular 16.0.0. 

In the past, we used the forRoot or forChild functions to initialize modules with specific providers or wrote some custom static factory functions to add configurability beyond providers. With Angular’s new standalone API and the elimination of NgModules, configuring lazy-loaded components or routes became much more straightforward.

Lazy Loaded Components

In our example, we have a simple configuration object SomeConfig that we want to provide under a specific injection token COMPONENT_CONFIG. We also export a default configuration for our eagerly loaded components.

				
					// config.model.ts

export interface SomeConfig {
  debug: boolean;
  text: string;
}

export const COMPONENT_CONFIG = new InjectionToken<SomeConfig>('Component Config');

export const DEFAULT_CONFIG: SomeConfig = { debug: false, text: 'Eager Component' };
				
			

To provide the default configuration with the COMPONENT_CONFIG token, we need to provide it at the application level. Within Angular’s new standalone project structure, we do it like this:

				
					//app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), {provide: COMPONENT_CONFIG, useValue:DEFAULT_CONFIG}]
};

//main.ts
bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));


				
			

We now create an eagerly loaded HomeComponent, assign a route, and start our application. Everything works as expected, and the default configuration gets displayed.

So far, so good. Let’s get some laziness in there, shall we?

To do so we create a copy of our HomeComponent and call it LazyHomeComponent. In the past, we achieved Iazy-loading like this.

				
					const routes: Routes = [
  {
    path: 'lazy',
    loadChildren: () =>
      import('../app/lazy-loaded-configurable/lazy-loaded-configurable.module').then(
        (m) => m.LazyLoadedConfigurableModule),
  },
];
				
			

We called loadChildren and imported our lazy module. Now without modules, we call loadComponent instead. It works like loadChildren worked before, with the difference that we now return a component instead of a module. Now we can lazy load our LazyHomeComponent. But how do we provide the configuration? Angular got our back. Angular 14 already introduced the possibility of applying a providers array to the Route definition. That means Angular creates a new EnvironmentInjector for this route. We can define and access specific services (or configurations) for that route level and all its children. Let’s add the provider’s array to the route configuration for the final result.

				
					//app.routes.ts

export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'lazy',
    loadComponent: () => import('./lazy-home/lazy-home.component'),
    providers: [{ provide: COMPONENT_CONFIG, useValue: LAZY_CONFIG }],
  }
];
				
			

Et voila. We successfully provided our custom lazy-configuration to the lazy-loaded component. 

You might have noticed that the component import above has no .then(c => c.LazyHomeComponent) clause. You can save this part if you add a default export at the end of the components file. So in our case, export default LazyHomeComponent. The same applies to files containing routes.

Lazy Loaded Routes

But it is not always the case that we want to configure each lazy-loaded component individually. Sometimes we want that good old module-like behavior. One configuration for one route segment and all its child routes/components. No problem. Let’s create a new folder with two more lazy components and define some route structure inside another file.

				
					//more-lazy.routes.ts

export const MORE_LAZINESS_CONFIG: SomeConfig = { debug: false, text: 'More Laziness' };

export const MORE_LAZINESS_ROUTES: Routes = [
  {
    path: '', 
    pathMatch: 'prefix', 
    providers: [{ provide: COMPONENT_CONFIG, useValue: MORE_LAZINESS_CONFIG }],
    children: [
      { path: 'lazy-1', component: LazyOneComponent },
      { path: 'lazy-2', component: LazyTwoComponent },
    ],
  },
];
				
			

Like in the old module days, we provide an empty base path and define two child routes for our components. To provide the configuration we use the same approach as in our lazy component example above and add some providers array with our configuration.

Now we need to lazy-load these routes. Let’s see how.

				
					export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'lazy',
    loadComponent: () => import('./lazy-home/lazy-home.component'),
    providers: [{ provide: COMPONENT_CONFIG, useValue: LAZY_CONFIG }],
  },
  {
    path: 'more-laziness',
    loadChildren: () => import('./more-laziness/more-lazy.routes'),
  },
];
				
			

We see that the good old loadChildren function is still there but it works slightly different. Istead of a module return value it now expects a Routes object. So we just need to reference our “bundled” child routes.

And after we start our application we can see that everything works nice and smooth.

Conclusion

Above, we explored configuring and lazily loading Angular components and routes and saw that it got even more accessible. The standalone approach and the possibility to provide on a route level are great addition to the framework to maintain reusable architecture combined with a pleasant developer experience.

GitHub Repository

You can find a sample implementation of the above concept right in this GitHub Repository:
https://github.com/thinktecture/blog-configure-lazy-modules-demo

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
.NET
KP-round
.NET 8 introduces a new Garbage Collector feature called DATAS for Server GC mode - let's make some benchmarks and check how it fits into the big picture.
09.10.2023