,

Migrating to Angular Standalone Components

Feb 11, 2023 reading time 8 minutes

Migrating to Angular Standalone Components

In this blog post, I want to describe how I migrated an application from ngModules to standalone components. Standalone components are available since Angular’s V14 and make an angular application possible without ngModules.

What are standalone components

Standalone components provide the possibility to create an Angular Application without ngModules. In the past, every component had to be registered in an ngModule to be available to an Angular application and to get all its dependencies. With standalone components, we can tell the component to declare all its dependencies by itself and also tell Angular to know about the component and its selector without an ngModule.

@Component({
  selector: 'app-about',
  templateUrl: './about.component.html',
  styleUrls: ['./about.component.css'],
})
@Component({
  selector: 'app-about',
  templateUrl: './about.component.html',
  styleUrls: ['./about.component.css'],
  standalone: true,
  imports: [...],
})

Dependencies are provided in an imports array in the metadata of the component now. That also means, that if a component wants to use other components in the template like

<app-navigation
  [loggedIn]="isLoggedIn$ | async"
  (dologin)="login()"
  (doLogout)="logout()"
  [doggoCount]="myDoggoCount$ | async"
></app-navigation>

<router-outlet></router-outlet>

<app-footer></app-footer>

In this case, the <app-navigation> and the <app-footer> component would need to be part of the import array like all the other modules, pipes etc. the standalone component needs.

@Component({
  selector: '...',
  templateUrl: '...',
  styleUrls: ['...'],
  standalone: true,
  imports: [FooterComponent, NavigationComponent, RouterModule, AsyncPipe],
})

So here is no need to distinguish between the imports array and the declarations array like we have seen in an ngModule before.

Pay attention that you can still import like the CommonModule if you do not want to deal with importing all the NgIf's and NgFor directly. As you wish for now :)

So what I did was migrating all the components to standalone: true and then removed them from the module and tried to make the app working again.

“But there is a schematic for this!” I know. I wanted to do it manually as it was my first steps. Furthermore, I wanted to see the errors, make the mistakes and fix them manually. This helps me to understand what is going on. Feel free to use the schematics here .

How about routing?

In the past, we have routed to an ngModule importing the routes of the module to show the component we wanted to. With standalone components, we can not route to a lazy loaded module anymore, because there is none.

This leads us to the question: How to separate features, if we can not separate them in modules anymore?

Honestly, this was one of the questions holding me back to get into standalone components until now, but it just did not make “click” for me, although the answer was right in front of me: I always grouped features like this:

.
├── container
│   ...
├── models
│   ...
├── presentational
│   ...
├── services
│   ...
├── store
│   ...
├── my-feature-routes.ts
└── my-feature.module.ts

And I loved the abstraction of a folder, having a module on top level organizing the feature. For not having a module anymore I went for an index file (barrel-patter like) to have the same grouping and “entrance” in the module, but without a module!

.
├── container
│   ...
├── models
│   ...
├── presentational
│   ...
├── services
│   ...
├── store
│   ...
└── index.ts

And the index.ts in this case holds everything which should be “visible” to the outside world. The routes in this case, which could be something like.

export const DOGGOS_ROUTES: Routes = [
  {
    path: '',
    component: MainDoggoComponent,   
  },
  {
    path: 'my',
    component: MyDoggosComponent,
  },
  {
    path: 'my/add',
    component: AddDoggoComponent,
  },
];

Which then can be lazy loaded with

export const APP_ROUTES: Routes = [
  {
    path: 'doggos',
    loadChildren: () => import('./doggos').then((m) => m.DOGGOS_ROUTES),
  },
  {
    path: 'eager-loaded-route',
    component: EagerComponent,
  },
];

So this problem is also solved. We still have our structured folders, but all on standalone components.

What about the app.module?

The app.module was always the first starting point and was used by the main.ts to bootstrap the application. Something like

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch((err) => console.error(err));

is very well known. However, without modules there is also no app.module present anymore so all the logic to start an application goes into the main.ts, and we will start the application bootstrapping a component instead of a module.

bootstrapApplication(AppComponent, {
  providers: [
    // All providers from the app.module go here
  ],
});

And that also means, that the providers also go into the providers array of that new bootstrapApplication module.

There is a helper method importProvidersFrom which you can use to import third party parts which rely on ngModules. That makes it easy to get started.

bootstrapApplication(AppComponent, {
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    provideRouter(APP_ROUTES),
    provideHttpClient(),
    importProvidersFrom(
      ToastrModule.forRoot({
        positionClass: 'toast-bottom-right',
      }),
      BrowserAnimationsModule
    ),
  ],
});

So we still can use the ToastrModule, although it uses modules. But also pay attention to the helper methods like provideHttpClient or provideRouter. These are helper methods from angular, which align with the new syntax.

Conclusion

I waited a little with migrating to standalone components because I struggled with understanding angular without modules, honestly. I loved modules because they led to a separation of features, and I could not think of this in another way. This migration showed me that there is absolutely no need of modules anymore. The migration is really smooth, and the application is more readable, and I still have the same separation as before. My concern was wrong. In the future, I will never start an application with modules again :-)

Links

PR with the Migration in my application on GitHub: https://github.com/FabianGosebrink/doggo-rate-app/pull/1/files

Standalone Migration by Tim Deschryver https://github.com/timdeschryver/ng-standalone-migration and https://timdeschryver.dev/blog/i-tried-the-angular-standalone-migration-and-here-is-the-result

Angular.io Standalone components https://angular.io/guide/standalone-components

Angular schematics to migrate to standalone https://github.com/angular/angular/blob/main/packages/core/schematics/ng-generate/standalone-migration/README.md