

I tried the Angular Standalone migration, and here is the result
source link: https://timdeschryver.dev/blog/i-tried-the-angular-standalone-migration-and-here-is-the-result#migration-examples
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

I tried the Angular Standalone migration, and here is the result


Last week Minko Gechev tweeted about an Angular schematic to automate the migration from @NgModules
to the standalone API. Of course, I had to try out this migration myself.
To test the migration I created a small Angular application, which I will use as a starting point for the migration. While the application is small, it contains a little bit of everything, child components, eager and lazy loaded modules, a pipe, a directive, a couple of tests, and a service.
The schematic is available starting from Angular version 15.2.0-next.2. To update to this version or a later version, run the following command:
npx ng update --next
npx ng update @angular/core --next
You can take a look at the before branch on GitHub if you're interested in the code from the example project.
To run the migration open a new CLI terminal at the root of the Angular project and run the @angular/core:standalone
schematic:
npx ng generate @angular/core:standalone
This gives you three options:
To complete the migration, you need to run all three options. Instead of migrating your whole codebase at once, you can also run the schematic on specific directories.
The first time I ran the schematic I tried to keep the application running and the tests green. For this, I had to manually update some parts of the code and tests (see the steps below). But while running the next options, I noticed that the schematic was also fixing some of the issues I had to fix manually. That's why I decided to run the schematic from the start again, but this time I ran the schematics after each other without updating the code and tests. Looking back at it, I think the latter is the way to go, although it seems not to be recommended in the Angular docs.
The schematic only migrates the code from NgModule
s to the new standalone API syntax.
But, lately Angular also added a bunch of new functional APIs.
For the completeness of this migration, I also manually migrated some features that are not covered by the schematic to their new equivalent functional API version.
If you're not interested in the details, you can take a look at the migrated version on the main branch (with manual changes between migration steps) or on the after branch (all migrations at once, and manual changes afterward).
Why you should migrate link
I think you should migrate to the standalone components because it has a few benefits.
The foremost is that Angular has a smoother learning curve for new developers. For new and experienced developers, a big advantage is a simplified codebase, which is easier to understand and maintain.
It also has a few performance benefits, e.g. you can lazy load a component because it defines its own dependencies explicitly.
Another benefit is that your tests require less setup code. In most cases, you only need to import the component you want to test and mock the external dependencies e.g. an HTTP service.
And who knows, perhaps somewhere in the future that Angular can automagically import the dependencies for you, and is step this step just an intermediate step to make that possible. But for now, you need to do it manually.
From the docs docs:
Standalone components provide a simplified way to build Angular applications. Standalone components, directives, and pipes aim to streamline the authoring experience by reducing the need for
NgModule
s. Existing applications can optionally and incrementally adopt the new standalone style without any breaking changes.
1. Convert all components, directives, and pipes to standalone link
Commit: d32df876bebc4f1824589bca14799cc27d6ff602:
Command link
npx ng generate @angular/core:standalone
Convert all components, directives, and pipes to standalone
Results link
- Components, directives, and pipes are migrated to the standalone version
- Dependencies are added to the standalone versions
NgModule
s are updated, e.g. components are moved from thedeclarations
to theimports
Manual changes link
- A child component referenced in a Route was not migrated. This was fixed in the next migration while running all schematics at once.
- Update TestBed: move standalone components/directives/pipes from
declarations
toimports
- Declarables are moved from the
declarations
to theimports
of anNgModule
Notes link
AppComponent
is not migrated- It also imports an internal
ɵInternalFormsSharedModule
module together with theFormsModule
orReactiveFormsModule
Migration Examples link
Components are migrated to standalone components:
standalone
is set totrue
- dependencies are added to
imports
import { Component } from '@angular/core';
import { JsonPlaceholderService } from '../services/json-placeholder.service';
+ import { SensitivePipe } from '../../shared-module/pipes/sensitive.pipe';
+ import { AsyncPipe, JsonPipe } from '@angular/common';
+ import { ɵInternalFormsSharedModule, FormsModule } from '@angular/forms';
+ import { MatInputModule } from '@angular/material/input';
+ import { MatFormFieldModule } from '@angular/material/form-field';
+ import { HighlightDirective } from '../../highlight-directive/highlight.directive';
@Component({
selector: 'app-lazy-child',
template: `
<div class="container">
<p><span appHighlight>lazy-child</span> works!</p>
<p>{{ 'eager-child works!' | sensitive }}</p>
<mat-form-field>
<mat-label>eager-child</mat-label>
<input matInput type="text" name="name" [(ngModel)]="form.name" />
</mat-form-field>
<pre>{{ todos$ | async | json }}</pre>
</div>
+ standalone: true,
+ imports: [HighlightDirective, MatFormFieldModule, MatInputModule, ɵInternalFormsSharedModule, FormsModule, AsyncPipe, JsonPipe, SensitivePipe]
export class LazyChildComponent {
form = {
name: '',
todos$ = this.placeholderService.getTodos();
constructor(private placeholderService: JsonPlaceholderService) {}
NgModule
s are updated by moving the declarations
to the imports
:
import { SensitivePipe } from './pipes/sensitive.pipe';
@NgModule({
- declarations: [SensitivePipe],
imports: [
CommonModule,
MatInputModule,
MatFormFieldModule,
imports: [
CommonModule,
MatInputModule,
MatFormFieldModule,
+ SensitivePipe,
exports: [MatInputModule, MatFormFieldModule, SensitivePipe]
export class SharedModule {}
2. Remove unnecessary NgModule classes link
Commit: c74471ae5b9627ab73ed0e163600834d4d51f85d
Command link
npx ng generate @angular/core:standalone
Remove unnecessary NgModule classes
Results link
- Files only containing an
NgModule
are deleted NgModules
s that reference the removedNgModule
s are updated
Manual changes link
- Update TestBed: remove deleted
NgModule
s - Commented a child component in
AppComponent
. This was fixed in the next migration while running all schematics at once.
Migration Examples link
The file shared.module.ts
is deleted because it only contained an NgModule
, SharedModule
:
NgModules
s that reference the removed NgModules
are updated.
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { LazyChildComponent } from './lazy-child/lazy-child.component';
- import { SharedModule } from '../shared-module/shared.module';
import { LazyRoutingModule } from './lazy-routing.module';
- import { HighlightModule } from '../highlight-directive/highlight.module';
@NgModule({
imports: [
CommonModule,
LazyRoutingModule,
- SharedModule,
FormsModule,
- HighlightModule,
LazyChildComponent
export class LazyModule {}
3. Bootstrap the application using standalone APIs link
Commit: 16c649d64130741ea75e4d35517ffd6b5b80cdc8
Command link
npx ng generate @angular/core:standalone
Bootstrap the application using standalone APIs
Result link
main.ts
is updated fromplatformBrowserDynamic().bootstrapModule(AppModule)
tobootstrapApplication(AppComponent)
Manual changes link
- Readded the child component that was removed in the previous step. This was not needed while running all the schematics at once.
Notes link
AppModule
still exists, but the content is commented out- It seems like files are imported by using the
\\
separator instead of/
Migration Examples link
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
+ import { importProvidersFrom } from '@angular/core';
+ import { AppComponent } from './app\\app.component';
+ import { provideAnimations } from '@angular/platform-browser/animations';
+ import { AuthConfigModule } from './app\\auth\\auth-config.module';
+ import { AppRoutingModule } from './app\\app-routing.module';
+ import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
+ import { AuthInterceptor } from 'angular-auth-oidc-client';
+ import { HTTP_INTERCEPTORS } from '@angular/common/http';
- platformBrowserDynamic().bootstrapModule(AppModule)
+ bootstrapApplication(AppComponent, {
+ providers: [
+ importProvidersFrom(BrowserModule, AppRoutingModule, AuthConfigModule),
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
+ provideAnimations()
.catch(err => console.error(err));
4. Remove unnecessary NgModule classes (for AppModule) link
Commit: c05ca76aad7717e303037e33c269602627ab9720
Command link
npx ng generate @angular/core:standalone
Remove unnecessary NgModule classes
Result link
- Now that
AppModule
is not used anymore, it is deleted
5. Migrate to provideRouter link
Commit: 528661c9cef1e9f3bf5cb83ff6571c96c4ae8164
This is not an automatic migration.
Result link
We can use provideRouter()
instead of RouterModule.forRoot()
and RouterModule.forChild()
.
For more info about provideRouter
see Angular Router Standalone APIs by Kevin Kreuzer.
Migration Examples link
import { importProvidersFrom } from '@angular/core';
import { AppComponent } from './app\\app.component';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(BrowserModule),
}).catch((err) => console.error(err));
6. Migrate to provideHttpClient link
Commit: 655217f3f528fc7db83515cfce59275043dd6183
This is not an automatic migration.
Result link
Instead of importing HttpClientModule
in AppModule
, and registering interceptors as providers with HTTP_INTERCEPTORS
we can now use provideHttpClient()
.
For more info about provideHttpClient
see The Refurbished HttpClient in Angular 15 – Standalone APIs and Functional Interceptors by Manfred Steyer.
Migration Examples link
import { bootstrapApplication } from '@angular/platform-browser';
- import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
AuthInterceptor,
authInterceptor,
} from 'angular-auth-oidc-client';
import { AuthConfigModule } from './app\\auth\\auth-config.module';
bootstrapApplication(AppComponent, {
providers: [
- importProvidersFrom(AuthConfigModule, HttpClientModule),
- { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
+ importProvidersFrom(AuthConfigModule),
+ provideHttpClient(withInterceptors([authInterceptor()])),
}).catch((err) => console.error(err));
7. Migrate to functional router guards link
Commit: 6b1977d24e9770871f432b0eaa0e24efd94d41fe
This is not an automatic migration.
Result link
A router guard that was implemented as a class can be refactored to a function.
For more info about functional router guards see How To Use Functional Router Guards in Angular by Dany Paredes .
It's probably best to immediately migrate to the new canMatch
guard, for more info see Introducing the CanMatch Router Guard In Angular by Netanel Basal.
Migration Examples link
Before:
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AuthorizationGuard implements CanActivate {
constructor(
private oidcSecurityService: OidcSecurityService,
private router: Router
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> {
return this.oidcSecurityService.isAuthenticated$.pipe(
map(({ isAuthenticated }) => {
// allow navigation if authenticated
if (isAuthenticated) {
return true;
// redirect if not authenticated
return this.router.parseUrl('');
After:
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { map } from 'rxjs';
export const authenticatedGuard = () => {
const router = inject(Router);
const securityService = inject(OidcSecurityService);
return securityService.isAuthenticated$.pipe(
map(({ isAuthenticated }) => {
// allow navigation if authenticated
if (isAuthenticated) {
return true;
// redirect if not authenticated
return router.parseUrl('');
8. Update tests, only import standalone components link
Commit: 7e04027511b8ece03522bb3e52e87775e4f7dd8a
This is not an automatic migration.
Result link
Because a component now contains all its dependencies, we can refactor the test cases. The test becomes simpler because we are not required to import all the dependencies anymore. Instead, we can import the component itself.
Migration Examples link
await TestBed.configureTestingModule({
imports: [
- MatFormFieldModule,
- MatInputModule,
- ReactiveFormsModule,
EagerChildComponent,
- SensitivePipe,
}).compileComponents();
const fixture = TestBed.createComponent(LazyChildComponent);
Support me
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK