Angular Standalone Components: migratie en best practices voor Angular 22

Volledige gids voor het migreren van Angular-applicaties van NgModules naar standalone components. Behandelt de officiële CLI-migratie in 3 stappen, selectorless components, OnPush als standaard en lazy loading met loadComponent in Angular 22.

Architectuur van de migratie naar Angular standalone components: overgang van NgModule- naar standalone-componentboom

Angular standalone components maken NgModules overbodig, verminderen boilerplate en maken fijnmazig lazy loading mogelijk in de hele applicatie. Sinds Angular 19 standalone tot standaard maakte en Angular 22 selectorless components als stabiel introduceerde, is het migreren van legacy modulegebaseerde codebases zowel eenvoudig als impactvol geworden.

Belangrijkste inzicht

Het officiële Angular CLI-schematic regelt het grootste deel van de migratie automatisch in drie passes. Een typische enterprise-applicatie kan de conversie binnen één sprint afronden, waarbij bundlegroottes met 30-50 % dalen dankzij lazy loading per component.

NgModules vs standalone components: wat is er veranderd

NgModules dienden sinds Angular 2 als de compilatiecontext voor componenten. Elk component, elke directive en elke pipe moest in precies één module worden gedeclareerd, en gedeelde functionaliteit vereiste zorgvuldig georchestreerde module-imports en -exports. Dit leidde tot strakke koppeling tussen onafhankelijke features en bemoeilijkte tree-shaking.

Standalone components draaien dit model om. Elk component declareert zijn eigen afhankelijkheden direct in de imports-array van de @Component-decorator. Geen moduleregistratie, geen gedeelde modules, geen barrel-exports van de halve applicatie. Vanaf Angular 22 wordt dit patroon verder vereenvoudigd met selectorless components, die directe imports zonder string-selectors mogelijk maken.

hero-list.component.tstypescript
import { Component, signal } from '@angular/core';
import { HeroCardComponent } from './hero-card.component';
import { SearchPipe } from '../pipes/search.pipe';

@Component({
  selector: 'app-hero-list',
  standalone: true,
  imports: [HeroCardComponent, SearchPipe],
  template: \`
    <div class="hero-grid">
      @for (hero of heroes() | search:query(); track hero.id) {
        <app-hero-card [hero]="hero" />
      }
    </div>
  \`
})
export class HeroListComponent {
  heroes = signal<Hero[]>([]);
  query = signal('');
}

De imports-array vervangt de volledige NgModule-afhankelijkheidsgraaf. De bundler ziet precies welke componenten, pipes en directives elk bestand nodig heeft, wat nauwkeurige tree-shaking mogelijk maakt. CommonModule is niet meer nodig in Angular 22 omdat de control flow-syntax (@for, @if) is ingebouwd in de compiler.

Het CLI-migratieproces in 3 stappen

Angular biedt een geautomatiseerd schematic dat de migratie in drie opeenvolgende passes afhandelt. Elke stap bouwt voort op de vorige, en het project moet tussen elke pass schoon compileren.

Stap 1: declaraties omzetten naar standalone

De eerste pass scant elk component, elke directive en elke pipe in het project, voegt standalone: true toe en verplaatst de noodzakelijke imports vanuit de bovenliggende NgModule naar de eigen imports-array van elk component.

bash
# Step 1: Convert all declarations to standalone
ng generate @angular/core:standalone --path=src/app

Kies bij de prompt "Convert all components, directives and pipes to standalone". Het schematic gebruikt statische analyse om afhankelijkheden te resolven, dus elk component met metadata die niet bij build-time geanalyseerd kunnen worden, wordt overgeslagen met een waarschuwing.

Stap 2: onnodige NgModules verwijderen

Nu alle declaraties standalone zijn, worden veel NgModules lege schillen. Deze pass identificeert modules die alleen standalone-declaraties opnieuw exporteerden en verwijdert ze.

bash
# Step 2: Remove empty NgModules
ng generate @angular/core:standalone --path=src/app

Kies "Remove unnecessary NgModule classes". Modules die nog providers, route-configuraties bevatten of door meerdere andere modules worden geïmporteerd, blijven behouden met een TODO-commentaar voor handmatige review.

Angular 22-vereiste

Angular 22 vereist TypeScript 6 en Node.js 26. Oudere versies worden niet ondersteund. Voer ng version uit om de compatibiliteit te controleren voordat de migratie begint.

Stap 3: overschakelen naar standalone bootstrap

De laatste pass vervangt de root-NgModule door Angular's bootstrapApplication-API en zet het root-component om naar standalone.

main.ts (after migration)typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));
app.config.tstypescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
};

Het ApplicationConfig-patroon vervangt de providers- en imports-arrays van de root-module. Alle provider-functies (provideRouter, provideHttpClient, provideAnimations) werken direct zonder module-wrappers.

Angular 22: OnPush als standaard en selectorless components

Angular 22 introduceerde twee wijzigingen die de ontwikkeling met standalone components beïnvloeden: OnPush als standaard change detection-strategie en selectorless components die stabiel zijn geworden.

OnPush als standaard

Nieuwe componenten die in Angular 22 worden gegenereerd, gebruiken standaard ChangeDetectionStrategy.OnPush. Dit moedigt signal-gebaseerde reactiviteit aan boven Zone.js-gestuurde checks. Tijdens de migratie voegt het schematic expliciet changeDetection: ChangeDetectionStrategy.Default toe aan bestaande componenten om achterwaartse compatibiliteit te behouden.

typescript
// Angular 22: OnPush is implicit for new components
@Component({
  selector: 'app-dashboard',
  standalone: true,
  // changeDetection: ChangeDetectionStrategy.OnPush is now the default
  template: \`<h1>{{ title() }}</h1>\`
})
export class DashboardComponent {
  title = signal('Dashboard');
}

Voor gemigreerde applicaties verdient het aanbeveling om OnPush stapsgewijs te adopteren door class properties te vervangen door signals. Dit sluit aan bij de signal-first architectuur die Angular 22 promoot.

Selectorless components (stabiel)

Selectorless components maken het mogelijk om componenten direct in templates te importeren zonder string-selectors te definiëren. Dit elimineert selector-naamconflicten in grote codebases en biedt betere type safety.

typescript
// Traditional approach with selector
@Component({
  selector: 'app-user-card',
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

// Selectorless approach (Angular 22+)
@Component({
  standalone: true,
  template: \`<div class="user-card">{{ user().name }}</div>\`
})
export class UserCardComponent {
  user = input.required<User>();
}

In templates worden selectorless components gebruikt via hun class-naam in plaats van een custom selector-tag. De compiler zorgt ervoor dat de import bestaat, waardoor fouten tijdens build-time worden opgemerkt in plaats van tijdens runtime.

Routing-migratie: van modules naar loadComponent

Routing-modules vereisen handmatige aandacht omdat het schematic loadChildren-module-imports niet automatisch omzet naar loadComponent of route-niveau loadChildren met standalone-routes.

Het legacy-patroon laadde hele feature-modules:

app.routes.ts (before)typescript
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module')
      .then(m => m.DashboardModule)
  }
];

Het standalone-equivalent laadt afzonderlijke componenten of routebestanden direct:

app.routes.ts (after)typescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(c => c.DashboardComponent)
  },
  {
    path: 'settings',
    loadChildren: () => import('./settings/settings.routes')
      .then(r => r.settingsRoutes)
  }
];
settings/settings.routes.tstypescript
import { Routes } from '@angular/router';

export const settingsRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./settings.component')
      .then(c => c.SettingsComponent),
    children: [
      {
        path: 'profile',
        loadComponent: () => import('./profile/profile.component')
          .then(c => c.ProfileComponent)
      },
      {
        path: 'security',
        loadComponent: () => import('./security/security.component')
          .then(c => c.SecurityComponent)
      }
    ]
  }
];

loadComponent lazy-laadt een enkel component. loadChildren met een routebestand lazy-laadt een heel feature-gebied. Beide produceren losse chunks die de browser op aanvraag ophaalt.

Klaar om je Angular gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Omgaan met SharedModules en gemeenschappelijke afhankelijkheden

SharedModules, de allesomvattende modules die veelgebruikte componenten, directives en pipes exporteren, vormen de meest voorkomende blokkade tijdens de migratie. Het schematic kan ze niet automatisch verwijderen omdat meerdere modules ze importeren.

De oplossing: gedeelde declaraties één voor één omzetten naar standalone en daarna de SharedModule verwijderen zodra niets ze nog importeert.

typescript
// Before: SharedModule re-exports everything
@NgModule({
  declarations: [LoadingSpinner, TooltipDirective, TruncatePipe],
  exports: [LoadingSpinner, TooltipDirective, TruncatePipe],
  imports: [CommonModule]
})
export class SharedModule {}

// After: Each declaration is standalone, import directly
// loading-spinner.component.ts
@Component({
  selector: 'app-loading-spinner',
  standalone: true,
  template: \`<div class="spinner" role="status"></div>\`
})
export class LoadingSpinner {}

Gebruikers importeren LoadingSpinner nu direct in plaats van de hele SharedModule. De bundler neemt alleen de specifieke componenten op die elke route nodig heeft.

Standalone-only ontwikkeling afdwingen

Na de migratie is het cruciaal te voorkomen dat nieuwe NgModules opnieuw in de codebase sluipen. Angular biedt hiervoor een TypeScript-compileroptie.

tsconfig.jsonjson
{
  "angularCompilerOptions": {
    "strictStandalone": true
  }
}

Met strictStandalone ingeschakeld levert elke poging om een niet-standalone component, directive of pipe te maken een compilatiefout op. Dit dwingt de nieuwe architectuur af binnen het hele team. De Angular forms-migratie volgt hetzelfde patroon, waarbij Signal Forms nu stabiel is in Angular 22.

Performancewinst: bundlegrootte en lazy loading

Het voornaamste performance-voordeel van standalone components komt van granulair lazy loading. Met NgModules werkte lazy loading op moduleniveau: één component uit een module importeren trok elke declaratie mee die de module exporteerde. Standalone components doorbreken die koppeling.

Een real-world benchmark op een middelgrote enterprise-applicatie (200+ componenten) toonde:

MetriekNgModule-gebaseerdStandaloneVerbetering
Initiële bundle485 KB218 KB-55 %
Grootste lazy chunk142 KB38 KB-73 %
Time to Interactive3,2 s1,8 s-44 %
Buildtijd (esbuild)12,4 s8,1 s-35 %

Deze cijfers komen voort uit het wegvallen van de overhead van module-resolutie en de mogelijkheid voor de bundler om ongebruikte exports op componentniveau te elimineren in plaats van op moduleniveau.

Standalone components testen met Vitest

Unittests vereenvoudigen aanzienlijk met standalone components. Angular 22 heeft Vitest de standaard testrunner gemaakt, ter vervanging van Karma. De TestBed-configuratie vereist niet langer het importeren van hele modules om de afhankelijkheden van een component te bevredigen.

hero-list.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroListComponent } from './hero-list.component';
import { HeroService } from '../services/hero.service';
import { of } from 'rxjs';

describe('HeroListComponent', () => {
  let fixture: ComponentFixture<HeroListComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [HeroListComponent],
      providers: [
        { provide: HeroService, useValue: { getHeroes: () => of([]) } }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(HeroListComponent);
  });

  it('should render hero cards', () => {
    fixture.componentRef.setInput('heroes', mockHeroes);
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-hero-card');
    expect(cards.length).toBe(mockHeroes.length);
  });
});

Het component gaat direct de imports-array van TestBed.configureTestingModule in. Alle gedeclareerde afhankelijkheden zijn al opgelost via het eigen imports, dus extra module-imports zijn niet nodig.

Veelvoorkomende migratievalkuilen

Circulaire imports tussen standalone components. Wanneer component A component B importeert en B importeert A, gooit de TypeScript-compiler een fout vanwege circulaire afhankelijkheid. De oplossing: extraheer de gedeelde interface naar een apart bestand of gebruik forwardRef() als tijdelijke workaround terwijl de afhankelijkheidsketen wordt gerefactord.

Externe libraries die nog NgModules gebruiken. Veel libraries zijn naar standalone gemigreerd, maar sommige legacy-pakketten exporteren nog NgModules. Importeer die modules direct in de imports-array van het standalone component, Angular ondersteunt het mengen van standalone- en modulegebaseerde imports.

Ontbrekende providers na het verwijderen van AppModule. Services die voorheen werden geleverd in de providers-array van de root-module moeten verhuizen naar de ApplicationConfig in app.config.ts of providedIn: 'root' gebruiken in de @Injectable-decorator. Route-scoped services moeten de providers-array in de routeconfiguraties gebruiken.

Sollicitatievraag

Wanneer in een Angular-sollicitatie naar standalone components wordt gevraagd, focus dan op drie punten: (1) dependency management verschuift van module- naar componentniveau, (2) tree-shaking wordt granulair, en (3) de testopzet vereenvoudigt omdat componenten hun eigen imports declareren.

Bronnen

Migreren naar standalone: belangrijkste punten voor Angular 22

  • Het officiële Angular CLI-schematic automatiseert 80-90 % van de migratie via drie opeenvolgende passes: declaraties omzetten, modules verwijderen, bootstrap omschakelen
  • Routing-modules vereisen handmatige conversie van loadChildren met NgModules naar loadComponent of standalone routebestanden
  • SharedModules zijn de voornaamste blokkade: zet elke gedeelde declaratie afzonderlijk om naar standalone en verwijder daarna de module
  • Schakel strictStandalone in tsconfig in om te voorkomen dat na de migratie nieuwe NgModules worden geïntroduceerd
  • Bundlegroottes dalen 30-55 % dankzij tree-shaking op componentniveau en granulair lazy loading met loadComponent
  • Unittests vereenvoudigen met Vitest als standaard runner: importeer het standalone component direct in TestBed zonder moduleconfiguratie
  • De OnPush-standaard en selectorless components van Angular 22 sluiten van nature aan op de standalone-architectuur voor een schonere, beter onderhoudbare codebase

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Dagelijkse challenge

Zie jij de bug in Angular?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 21 augustus 2026

Tags

#angular
#standalone components
#migration
#angular 22
#ngmodule
#lazy loading

Delen

Gerelateerde artikelen