Reusable layouts in Angular

November 16, 2019

Note 📌
(2020-11-23) I still use this trick for my Angular projects and the more I use other technologies like Nuxt.js or Next.js, the more I think this pattern helps to design great front-end architectures.

When building JavaScript applications we usually split components in different layers, each one responsible of its concern. You’ve certainly heard about presentational components, container components, or the less well known layout components?

Layout components are used to hold common layout composition. This design enables reusing layouts across different parts of your application. It also simplify underlying components template and enforce the single responsibility principle.

The view layer architecture

The schema below illustrates the component tree using a layout component. Layout components are realizable using the nested <router-outlet> technique.

layout schema

Now let’s see what does it look like in code. Here is the root AppComponent that instantiates the first <router-outlet>.

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

@Component({
  selector: 'app-root',
  template: `<router-outlet></router-outlet>`,
})
export class AppComponent {}

Then we need to declare top level routes. Note that lazy loading is used to improve initial load performance.

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

export const APP_ROUTES: Route[] = [
  {
    path: '',
    redirectTo: '/dashboard',
    pathMatch: 'full',
  },
  {
    path: 'dashboard',
    loadChildren: () =>
      import('./dashboard/dashboard.module').then((m) => m.DashboardModule),
  },

  /* No layout routes */
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },

  /* Not found redirection */
  { path: '**', redirectTo: '' },
];

At this point we have to bring this together in the AppModule.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';

import { APP_ROUTES } from './routes';
import { AppComponent } from './app.component.ts';

@NgModule({
  imports: [BrowserModule, RouterModule.forRoot(APP_ROUTES)],
  bootstrap: [AppComponent],
})
export class AppModule {}

The next step is to create the layout component. Consider this simple scenario.

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

@Component({
  selector: 'app-main-layout',
  template: `
    <app-navbar></app-navbar>
    <router-outlet></router-outlet>
    <app-footer></app-footer>
  `,
})
export class MainLayoutComponent {}

The nested <router-outlet> is declared in the MainLayoutComponent. The router will pass-through this layout component to resolve the child component.

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

import { DashboardComponent } from './dashboard.component';
import { MainLayoutComponent } from './main-layout.component';

export const DASHBOARD_ROUTES: Route[] = [
  {
    path: '',
    component: MainLayoutComponent,
    children: [
      {
        path: '',
        component: DashboardComponent,
      },
    ],
  },
];

The piece of code above stick all together, layout and container components are combined in a declarative way using the router tree. Imagine we want to swap the MainLayoutComponent with an other layout, we can easily achieve this without refactoring the DashboardComponent template, which is pretty cool.

Note that using this technique, the router re-creates layout components only when the user navigates between routes from different layouts.

Resources

Here is an interacting example created by Josip Bojčić using the nested router trick and multiple layouts.

There is an other approach using router events and router data. This approach doesn’t come with a nested <router-outlet> but at the end it looks less robust. We cannot create multiple layouts because it relies on conditional templating.

Blog ideas

Upvote what you'd like me to write about next.

  • Nx DTE vs CI job matrix
    Nx Distributed Task Execution (DTE) vs a traditional CI job matrix with manual sharding: a comparison of developer experience, performance, and reliability.
  • Are you ready for a monorepo?
    An opinionated checklist to help you decide whether a monorepo is the right choice for your organization, and how to prepare for the transition.
  • Why monorepos unlock AI coding agents
    Shared context from frontend to backend, consistent conventions, and reliable task graphs: what makes a monorepo the ideal substrate for AI-assisted development.
  • Managing CI flakiness at scale
    Detecting, quarantining, and fixing flaky tests without slowing the pipeline: patterns that survive past 25 engineers.
  • Monitoring CI health
    Tools and techniques for tracking CI performance and reliability over time, and alerting on regressions before they impact developers.
Edouard Bozon

I'm Edouard Bozon, a passionate software engineer based in France, specializing in monorepo architectures, platform engineering, DevOps, and infrastructure. I build tools and workflows that help engineering teams scale their codebases and ship software with confidence.