Angular PrimerAngular 22 · 2022 → 2026

Standalone components

No `NgModule`, no `declarations` array, no shared module to keep in sync. A component imports exactly what its own template uses.

What a component looks like now

A component is a class plus a template plus the list of things that template needs. That is the whole idea:

2026 — a standalone component
import { Component, signal } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-topic-nav',
  imports: [RouterLink],
  template: `
    <nav>
      @for (link of links(); track link.path) {
        <a [routerLink]="link.path">{{ link.label }}</a>
      }
    </nav>
  `,
})
export class TopicNav {
  readonly links = signal([{ path: '/topics/signals', label: 'Signals' }]);
}

Three things to notice, because each one used to be somewhere else:

  • imports: [RouterLink] declares a dependency of this template. There is no module to put it in, and no other component can accidentally rely on this one importing it.
  • No standalone: true. Standalone is the default since v19; the flag is on its way out. Writing it is harmless, not writing it is expected.
  • No providers in a module. Anything the component needs comes from inject() or from app.config.ts.

How the app starts

The bootstrap moved out of AppModule and into a function call. main.ts is now three lines you can actually read:

2026 — main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';

bootstrapApplication(App, appConfig).catch((err) => console.error(err));

Everything the old AppModule imported goes into app.config.ts as a provider list:

2026 — app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
    provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
  ],
};

2022 — the module that did all of this

@NgModule({ declarations: [AppComponent, TopicNavComponent], imports: [BrowserModule, RouterModule.forRoot(routes), HttpClientModule], providers: [{ provide: API_URL, useValue: '/api' }], bootstrap: [AppComponent], }) export class AppModule {} // main.ts platformBrowserDynamic().bootstrapModule(AppModule);

2026 — the same app, no module

// app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideRouter(routes), provideHttpClient(withFetch()), { provide: API_URL, useValue: '/api' }, ], }; // main.ts bootstrapApplication(App, appConfig);

The interesting difference is not the line count, it is that the provider list is data. You can spread it, compose it, or hand a slice of it to a test without touching a module class.

Lazy loading without modules

loadChildren pointing at a module is gone in most code; loadComponent points straight at the component that will be rendered:

2026 — a lazy route
export const routes: Routes = [
  { path: '', component: Home, title: 'Angular Primer' },
  {
    path: 'topics/:slug',
    loadComponent: () => import('./pages/topic/topic').then((m) => m.Topic),
    title: (route) => `${route.paramMap.get('slug')} — Angular Primer`,
  },
];

If a feature has several routes, loadChildren can now load a routes array instead of a module — loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES) — so a feature slice is a file of routes, not a module wrapper around one.

Styles and metadata

styleUrl (singular) is the modern spelling; styleUrls: ['x.css'] still works but is redundant for a single file. A few other v22 defaults are worth knowing:

  • OnPush is the default for components the CLI generates. With signals and zoneless, this is not a risk — a component that reads a signal is checked when that signal changes.
  • changeDetection can still be set explicitly if you are porting something that relies on the old behaviour.
  • encapsulation is still emulated by default, so your component styles are scoped as before.

Migrating the modules you have

You do not have to convert anything by hand. The CLI ships a schematic that rewrites modules into standalone components, adds the missing imports, and removes the module declarations:

ng generate @angular/core:standalone
# then, once every component is standalone:
ng generate @angular/core:standalone --mode=prune-ng-modules
# and for a module-based bootstrap:
ng generate @angular/core:standalone --mode=standalone-bootstrap

Run it on a branch, run the tests, and expect the schematic to occasionally add an import that is technically unused — the compiler will tell you (NG8113) and the fix is to delete the line.

The one mistake that bites in the standalone world is reachability. A pipe or directive that used to arrive via SharedModule is now invisible unless the component that uses it lists it in imports. The error is explicit — "'X' is not a known element" in a template — but if your shared module was doing double duty as a dumping ground, the honest answer is to import what you actually use, per component.

What to take away

A component is now a self-contained unit you can read top to bottom: its template, what the template needs, and how it gets its data. Nothing is provided by ambient module state, so nothing breaks because someone else edited a shared module. Next: what gets in and out of that component, in The component contract.