Standalone in Angular. Is it time to migrate?

Sergey Gultyayev
6 min readMay 7, 2023

--

This fall Angular standalone API came out of developer preview which means that it’s now completely safe to use and the API is stable and won’t change drastically in the foreseeable future.

Photo by Ross Sneddon on Unsplash

NgModules

We have had NgModules for years. They are the key building blocks of Angular applications which allow it to be split in chunks by leveraging lazy-loading and tree-shaking. Angular app has to have at least one NgModule which is usually the default AppModule — where the whole app begins. It declares which component will be mounted to the DOM and all of its dependencies in the metadata.

If you wanted a great tree-shakeability in your shared library — you would have to create an NgModule for each single component. This created a lot of boilerplate. The component could fit in 20 lines of code while being inline, but it still required creating a module which would declare this component and export it.

Why we have NgModules

Compared to React where you import every single component in the file where you need to use it in Angular we declare components in a module. Then we implicitly use a component’s selector in the template and it’s corresponding component resolved by looking up into the modules. Furthermore, with NgModules we can declare providers of this module which will enable us to inject an instance of a provider in the context. So NgModules play a role of an injection context as well. They are a stop point for the DI to lookup for a provider in.

Good to know. Imported modules and eagerly loaded modules in the router result in one single module in the final app. That’s why when you import HttpClientModule in the AppModule its provider HttpClient becomes available across your whole application.
Lazy loaded modules are not merged in to the main module due to the obvious reason (they are loaded lazily) and that’s why providers of theirs are not available outside of their scope.

What is standalone API

Standalone API enables us to get rid of those boilerplate modules which only responsibility was to declare and export one single component. It works with components, directives and pipes. To enable it you need to specify in the metadata standalone: true which effectively transforms your component into a combination of a component and a module. So basically from the Angular’s perspective standalone component is (almost) the same as a component + module.

// app-button.component.ts
@Component({
selector: 'button[appButton]',
template: '<ng-content />',
styles: [ /* ... */ ],
})
export class AppButtonComponent { /* ... */ }

// app-button.module.ts
@NgModule({
declarations: [ AppButtonComponent ],
exports: [ AppButtonComponent ]
})
export class AppButtonModule {}

// ----
// vs
// ----

@Component({
selector: 'button[appButton]',
template: '<ng-content />',
standalone: true,
styles: [ /* ... */ ],
})
export class AppButtonComponent { /* ... */ }

Now, instead of having multiple files and remembering that you need also to export your component from its module to be available in the importing module you can simply use one single component file and declare it as standalone. The usage then goes the same (it’s still a module). You need to import your component.

So if we wanted to use the button in the AppModule we would write the following

@NgModule({
/* ... */
imports: [
/* ... */
AppButtonComponent,
})
export class AppModule {}

Now, the component is available for usage as usual.

Another great thing about standalone API is that they are completely interoperable with existing applications meaning that you can use standalone components/directives/pipes in a non standalone-app and vice-versa.

There is still some work going on to enable a standalone-only app to be fully operable on SSR.

Standalone app

It’s possible to write application without modules altogether by using functions introduced in v15.

import {bootstrapApplication} from '@angular/platform-browser';
import {ImageGridComponent} from'./image-grid';

@Component({
standalone: true,
selector: 'photo-gallery',
imports: [ImageGridComponent],
template: `
… <image-grid [images]="imageList"></image-grid>
`,
})
export class PhotoGalleryComponent {
// component logic
}

bootstrapApplication(PhotoGalleryComponent);

This is a single-file bootstrap standalone app example from the official Angular blog. Here, we need way fewer steps for bootstrap (no platformBrowserDynamic , no NgModule, no bootstrap in the metadata) than in a regular NgModule-based app.

Also, since our components are also modules Angular router can now lazy load routes declarations (they have to be standalone as you cannot use a component without its declaration).

// app.routes.ts
export const appRoutes: Routes = [{
path: 'lazy',
loadChildren: () => import('./lazy/lazy.routes')
.then(routes => routes.lazyRoutes)
}];

// lazy.routes.ts
import {Routes} from '@angular/router';

import {LazyComponent} from './lazy.component';

export const lazyRoutes: Routes = [{path: '', component: LazyComponent}];

// app.component.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(appRoutes)
]
});

Furthermore, now you can also lazy load one single component instead of the whole module which has its own routing.

{
path: 'lazy',
loadComponent: () => import('./lazy-file').then(m => m.LazyComponent),
}

Moreover, now you can also declare a providers array right on the route path definition. Thanks to that we can see all the route-scoped providers right from the routes definition. Also, thanks to specifying the providers array you no longer need lazy loading to have providers scoped to a subset of routes.

Working with providers

Since we still have old NgModules which we need to work with and they may contain services we need a way to get those providers extracted. For this purpose Angular team has introduced an importProvidersFrom function.

Now if you want to use providers which are declared in an NgModule you write providers: [ importProvidersFrom(SomeModule) ] and it will import the providers.

Migrating to standalone

One of my most loved things in Angular is that on every major update they provide us with schematics for an easy migration experience. This update is no exception even though it’s not a breaking change and Angular doesn’t seem to be migrating from NgModules. It’s rather an alternative and not a replacement. If you want to migrate your app you can follow to the official docs for guidance on that.

Is it worth migrating?

In my personal opinion it very depends. As mentioned above standalone is not the new way of building apps which is supposed to replace the NgModules, but rather an alternative for boilerplate. I would migrate shared libraries to standalone, because usually a good library consists of N components and N modules which exclusively export their corresponding component. Furthermore, they usually are not relying on lots of other imports themselves. Thus, you get less files, less boilerplate and a better DX when consuming the library.

Keep in mind that if you are a user of the infamous SharedModule standalone API won’t make any difference in bundle size for you as you are still importing everything everywhere.

However, if we are talking about migration of an application and not a library then I’d advise against it. Here is why.

Applications usually use components which themselves use other components etc. This would result in your *.component.ts file bloated not only with import statements on top of the file which some developers dislike but also imports array right in the component’s metadata. Furthermore, each single component will be bloated with those imports which doesn’t look appealing.

With NgModules it’s easier because that file is specialised in containing all of that technical data that is required by the framework to operate correctly and you don’t need to visit it yourself manually. IDEs automatically update imports array when you use a component from another module, schematics update declarations array when you create a component. In other words, all of this stuff is taken away from your eyes into a separate file which you rarely open yourself. Thus it’s easier to work.

However, some developers have reported on Twitter that their app became more manageable and better split into chunks after the migration as it enabled them to eliminate unused imports.

In the end of the day standalone API on its own doesn’t bring you any features that you didn’t have with NgModules. They eliminate some boilerplate in exchange for part of this boilerplate to become part of the component metadata. Therefore, whether to migrate the app is up to you, but if you don’t — you are not missing any crucial updates. For the end user it will be the same. So the app design is the key factor here. Standalone is more like a syntax sugar (IMO).

--

--

Sergey Gultyayev
Sergey Gultyayev

Written by Sergey Gultyayev

A front-end developer who uses Angular as a main framework and loves it

No responses yet