DEV Community

Connie Leung
Connie Leung

Posted on

45

How to lazy-load routes and import standalone components in Angular

Introduction

In Angular, routing is a key feature of application to navigate users to different pages to render components. A typical enterprise Angular application would lazy load routes and standalone components to maintain small main bundle to reduce the initial load time. In advanced case, such application even loads root-level and children routes to render parent and children components.

In this blog post, I would like to cover three cases of lazy loading single route and standalone component:

  • Lazy load default route
  • Lazy load routes that exist in the route array
  • Lazy load non-existing route

Define application route array

// app.route.ts

import { Route } from '@angular/router';

// lazy-load standalone component
export const APP_ROUTES: Route[] = [{
  path: 'pokemon',
  loadComponent: () => import('./pokemon/pokemon/pokemon.component')
    .then(mod => mod.PokemonComponent)
}, 
{
  path: '',
  pathMatch: 'full',
  loadComponent: () => import('./home/home.component').then(mod => mod.HomeComponent)
},
{
  path: '**',
  loadComponent: () => import('./page-not-found/page-not-found.component')
    .then(mod => mod.PageNotFoundComponent)
}];
Enter fullscreen mode Exit fullscreen mode

In app.route.ts file, I defined a route array and the application should lazy load the routes when they are clicked. When the path of the route is /pokemon, Angular imports PokemonComponent. When users reach the default URL, they should see HomeComponent standalone component. For other non-existent routes, Angular imports PageNotFoundComponent that redirects to home after 10 seconds.

Set up routes in the application

// main.ts

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [RouterLink, RouterOutlet],  // import RouteLink and RouterOutlet to use in inline template
  template: `
    <ul>
      <li><a routerLink="/" routerLinkActive="active">Home</a></li>
      <li><a routerLink="/pokemon" routerLinkActive="active">Show Pokemon</a></li>
      <li><a routerLink="/bad" routerLinkActive="active">Bad route</a></li>
    </ul>
    <router-outlet></router-outlet>
  `,
  styles: [`...omitted due to brevity...`]
})
export class App {}

bootstrapApplication(App, {
  providers: [
    provideHttpClient(),
    // provider to inject routes, preload all modules and trace route change events
    provideRouter(APP_ROUTES, withPreloading(PreloadAllModules), withDebugTracing())
  ]
});
Enter fullscreen mode Exit fullscreen mode

In main.ts, I provided APP_ROUTES to provideRouter provider. Moreover, the provider has two features; withPreloading that preloads all modules and withDebugTracing that traces routes in debug mode.

In the inline template, each hyperlink has routeLink directive that specifies the path and <router-outlet> is the placeholder of the standalone component. Finally, the page renders three routes for clicking.

Home Page

Lazy load default route

When browser first loads the application or user clicks 'Home', routeLink equals to '/' and it meets path: '' condition. Therefore, HomeComponent is imported

{
  path: '',
  pathMatch: 'full',
  loadComponent: () => import('./home/home.component').then(mod => mod.HomeComponent)
}
Enter fullscreen mode Exit fullscreen mode
// home.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  standalone: true,
    template: `
    <div>
      <h2>Click Pokemon Link</h2>
    </div>
  `,
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class HomeComponent {}
Enter fullscreen mode Exit fullscreen mode

The simple component displays "Click Pokemon Link" header and serves its only purpose.

Lazy load routes that exist

When user clicks "Show Pokemon", routeLink changes to "/pokemon" and it matches the route path of "pokemon".

{
  path: 'pokemon',
  loadComponent: () => import('./pokemon/pokemon/pokemon.component')
    .then(mod => mod.PokemonComponent)
}
Enter fullscreen mode Exit fullscreen mode

The route eventually imports Pokemon component that also renders the rest of the child components.

// pokemon.component.ts

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [AsyncPipe, NgIf, PokemonControlsComponent, PokemonPersonalComponent, PokemonTabComponent],
  template: `
    ...render HTML and child components...  
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  retrievePokemon = retrievePokemonFn();
  pokemon$ = getPokemonId().pipe(switchMap((id) => this.retrievePokemon(id)));
}
Enter fullscreen mode Exit fullscreen mode

We hit a bad route, how do we fix it?

In the last scenario, user clicks "Bad route" but "/bad" does not exist in the routes array. Will the application break? Fortunately, I can define path: "**" route that catches all unmatched paths.

{
  path: '**',
  loadComponent: () => import('./page-not-found/page-not-found.component')
    .then(mod => mod.PageNotFoundComponent)
}
Enter fullscreen mode Exit fullscreen mode

This route renders PageNotFoundComponent and informs users that the page does not exist.

// page-not-found.component.ts

@Component({
  selector: 'app-page-not-found',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <div>
      <h2>Page not found</h2>
      <p>Return home after {{ countDown$ | async }} seconds</p>
    </div>
  `,
  styles: [`...omitted due to brevity...`]
})
export class PageNotFoundComponent implements OnInit {
  countDown$ = timer(0, 1000)
    .pipe(
      map((value) => 10 - value),
      takeWhile((value) => value >= 0),
      shareReplay(1),
    );

  redirectHome$ = this.countDown$.pipe(
      tap((value) => {
        if (value <= 0) {
          inject(Router).navigate(['']);
        }
      }),
      takeUntilDestroyed()
    );

  ngOnInit(): void {
    this.redirectHome$.subscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

This component displays "Page not found" message and redirects home after 10 seconds.

The following Stackblitz repo shows the final results:

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

Neon image

Serverless Postgres in 300ms (❗️)

10 free databases with autoscaling, scale-to-zero, and read replicas. Start building without infrastructure headaches. No credit card needed.

Try for Free →

Top comments (0)

Image of Stellar post

How a Hackathon Win Led to My Startup Getting Funded

In this episode, you'll see:

  • The hackathon wins that sparked the journey.
  • The moment José and Joseph decided to go all-in.
  • Building a working prototype on Stellar.
  • Using the PassKeys feature of Soroban.
  • Getting funded via the Stellar Community Fund.

Watch the video

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay