[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_300x300
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
pg
While basic value objects solve primitive obsession, complex domain requirements need sophisticated modeling techniques. This article explores advanced patterns using Thinktecture.Runtime.Extensions to tackle real-world scenarios: open-ended dates for employment contracts, composite file identifiers across storage systems, recurring anniversaries without year components, and geographical jurisdictions using discriminated unions.
19.10.2025
.NET
pg
Domain models often involve concepts that exist in multiple distinct states or variations. Traditional approaches using enums and nullable properties can lead to invalid states and scattered logic. This article explores how discriminated unions provide a structured, type-safe way to model domain variants in .NET, aligning perfectly with Domain-Driven Design principles while enforcing invariants at the type level.
06.10.2025
.NET
pg
Learn how to seamlessly integrate Smart Enums with essential .NET frameworks and libraries. This article covers practical solutions for JSON serialization, ASP.NET Core model binding for both Minimal APIs and MVC controllers, and Entity Framework Core persistence using value converters. Discover how Thinktecture.Runtime.Extensions provides dedicated packages to eliminate integration friction and maintain type safety across your application stack.
21.09.2025