1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?
  3. 4 Ways to Prepare for Angular’s Upcoming Standalone Components
  4. Routing and Lazy Loading with Angular’s Standalone Components

Routing and Lazy Loading with Angular’s Standalone Components

For lazy loading, we can directly point to further routing configurations but also to Standalone Components.

  1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?
  3. 4 Ways to Prepare for Angular’s Upcoming Standalone Components
  4. Routing and Lazy Loading with Angular’s Standalone Components

Since its first days, the Angular Router has always been quite coupled to NgModules. Hence, one question that comes up when moving to Standalone Components is: How will routing and lazy loading work without NgModules? This article provides answers and also shows, why the router will become more important for Dependency Injection.

Big thanks to Pawel Kozlowski from the Angular team for proofreading this article!

📂 Source Code (CLI Example)

📂 Source Code (Nx Example)

Providing the Routing Configuration

When bootstrapping a standalone component, we can provide services for the root scope. For the sake of backwards compatibility, the function importProvidersFrom allows to import them from existing NgModules:

// main.ts

import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { APP_ROUTES } from './app/app.routes';
[...]

bootstrapApplication(AppComponent, {
  providers: [
    // Configuring the Router
    importProvidersFrom(RouterModule.forRoot(APP_ROUTES)),

    importProvidersFrom(StoreModule.forRoot(reducer)),
    importProvidersFrom(EffectsModule.forRoot()),
    importProvidersFrom(StoreDevtoolsModule.instrument()),

    importProvidersFrom(HttpClientModule),
  ]
});

This bridges the gap between NgModules and Standalone Components. The shown example uses this concept to provide services for the router, NGRX, and the HttpClientService. As usual, the used RouterModule is called with forRoot which takes the top-level router configuration.

Please note, that the usage of importProvidersFrom will peak off over time, as more and more libraries will provide functions for directly configuring their providers. For instance, there is the idea of providing a provideRouter function for setting up the router:

providers:[
    provideRouter(APP_ROUTES)    
]

The exact name of these function will be defined soon. While naming is hard in general, this is also critical because it’s vital to establish a naming pattern for such methods. Similar to forRoot, forChild, or forFeature, Angular developers should know for which function to look out when adding a new library.

Lazy Loading with Standalone Components

In the past, a lazy route pointed to an NgModule with child routes. As there are no NgModules anymore, loadChildren can now directly point to a lazy routing configuration:

// app.routes.ts

import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

export const APP_ROUTES: Routes = [
    {
        path: '',
        pathMatch: 'full',
        redirectTo: 'home'
    },
    {
        path: 'home',
        component: HomeComponent
    },

    // Option 1: Lazy Loading another Routing Config
    {
        path: 'flight-booking',
        loadChildren: () =>
            import('./booking/flight-booking.routes')
                .then(m => m.FLIGHT_BOOKING_ROUTES)
    },

    // Option 2: Directly Lazy Loading a Standalone Component
    {
        path: 'next-flight',
        loadComponent: () => 
            import('./next-flight/next-flight.component')
                .then(m => m.NextFlightComponent)
    },
    [...]
];

This removes the indirection via an NgModule and makes our code more explicit. As an alternative, a lazy route can also directly point to a Standalone Component. For this, the above shown loadComponent property is used.

I expect that most teams will favor the first option, because normally, an application needs to lazy loading several routes that go together.

Environment Injectors: Services for Specific Routes

With NgModules, each lazy module introduced a new injector and hence a new injection scope. This scope was used for providing services only needed by the respective lazy chunk. An example is setting up NGRX features.

To cover such use cases, the Router now allows for introducing providers for each route. These services can be used by the route in question and their child routes:

// booking/flight-booking.routes.ts

export const FLIGHT_BOOKING_ROUTES: Routes = [{
    path: '',
    component: FlightBookingComponent,
    providers: [
        importProvidersFrom(StoreModule.forFeature(bookingFeature)),
        importProvidersFrom(EffectsModule.forFeature([BookingEffects])),
    ],
    children: [
        {
            path: '',
            pathMatch: 'full',
            redirectTo: 'flight-search'
        },
        {
            path: 'flight-search',
            component: FlightSearchComponent
        },
        {
            path: 'passenger-search',
            component: PassengerSearchComponent
        },
        {
            path: 'flight-edit/:id',
            component: FlightEditComponent
        }
    ]
}];

As shown here, we can provide services for several routes by grouping them as child routes. In these cases, a component-less parent route with an empty path (path: '') is used. This pattern is already used for years to assign Guards to a group of routes.

Technically, using adding a providers array to a router configuration introduces a new injector at the level of the route. Such an injector is called Environment Injector and replaces the concept of the former (Ng)Module Injectors. The root injector and the platform injector are further Environment Injectors.

Interestingly, this also decouples lazy loading from introducing further injection scopes. Previously, each lazy NgModule introduced a new injection scope. Now, lazy loading itself doesn’t influence the scopes. Instead, you define new scopes by adding a providers array to your routes. Also, these routes can but don’t need to be lazy.

Setting up Your Environment: ENVIRONMENT_INITIALIZER

Some libraries used the constructor of lazy NgModule for their initialization. To further support this approach without NgModules, there is now the concept of an ENVIRONMENT_INITIALIZER:

export const FLIGHT_BOOKING_ROUTES: Routes = [{
    path: '',
    component: FlightBookingComponent,
    providers: [
        importProvidersFrom(StoreModule.forFeature(bookingFeature)),
        importProvidersFrom(EffectsModule.forFeature([BookingEffects])),
        {
            provide: ENVIRONMENT_INITIALIZER,
            multi: true,
            useValue: () => inject(InitService).init()
        }
    ],
    children: [
        [...]
    ]
}

Basically, the ENVIRONMENT_INITIALIZER provides a function executed when the Environment Injector is initialized. The flag multi: true already indicates that you can have several such initializers per scope.

Providers Setup as a Black Box?

There are situations, where you want to use services that are setup inside a library. While the services are part of the library’s public API, their setup remains an implementation detail.

We encountered such a situation before, when we discussed the idea of a configureRouter function provided by the Angular Router. Another example for this comes up when using a layered architecture as seen in lots of Nx workspaces nowadays:

Providers are configured by the domain lib and used in the feature lib

Here, the domain library manages the state for one or several feature libraries. For this, it uses an NGRX feature slice. As the domain library does not use routing, we cannot configure the NGRX feature slice there. Nethertheless, only the domain library knows how to set it up.

Hence, the domain library only provides a function returning the needed providers:

// libs/booking/domain/src/lib/domain.providers.ts

[...]

export function forBookingDomain() {
    return [
        importProvidersFrom(StoreModule.forFeature(bookingFeature)),
        importProvidersFrom(EffectsModule.forFeature([BookingEffects])),
    ];
}

This function is used for setting up an Environment Injector in the feature library:

// libs/booking/feature-book/src/lib/flight-booking.routes.ts

import { Routes } from '@angular/router';
import { forBookingDomain } from '@nx-example/booking/domain';
[...]

export const FLIGHT_BOOKING_ROUTES: Routes = [{
    path: '',
    component: FlightBookingComponent,
    providers: [
        forBookingDomain()
    ],
    children: [
        {
            path: '',
            pathMatch: 'full',
            redirectTo: 'flight-search'
        },
        {
            path: 'flight-search',
            component: FlightSearchComponent
        },
        {
            path: 'passenger-search',
            component: PassengerSearchComponent
        },
        {
            path: 'flight-edit/:id',
            component: FlightEditComponent
        }
    ]
}];

If several features needed to use the same state, we could also pull the call to forBookingDomain up to the app level.

What’s next? More on Architecture!

So far, we’ve seen how to decompose a huge client into several libraries with Standalone Components. However, when dealing with enterprise-scale frontends, several additional questions come in mind:

  • According to which criteria can we subdivide a huge application into sub-domains?
  • How can we make sure, the solution is maintainable for years or even decades?
  • Which options from Micro Frontends are provided by Module Federation?

Our free eBook (about 120 pages) covers all these questions and more:

free ebook

Feel free to download it here now!

Don't Miss Anything!


Subscribe to our newsletter to get all the information about Angular.


* By subscribing to our newsletter, you agree with our privacy policy.

Unsere Angular-Schulungen

  1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?
  3. 4 Ways to Prepare for Angular’s Upcoming Standalone Components
  4. Routing and Lazy Loading with Angular’s Standalone Components

Aktuelle Blog-Artikel

  1. Angular’s Future Without NgModules – Part 1: Lightweight Solutions Using Standalone Components
  2. Angular’s Future Without NgModules – Part 2: What Does That Mean for Our Architecture?
  3. 4 Ways to Prepare for Angular’s Upcoming Standalone Components
  4. Routing and Lazy Loading with Angular’s Standalone Components

Nur einen Schritt entfernt!

Stellen Sie noch heute Ihre Anfrage,
wir beraten Sie gerne!

Jetzt anfragen!