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:
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
providersin a module. Anything the component needs comes frominject()or fromapp.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:
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:
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
],
};2022 — the module that did all of this
2026 — the same app, no module
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:
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:
OnPushis 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.changeDetectioncan still be set explicitly if you are porting something that relies on the old behaviour.encapsulationis 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-bootstrapRun 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.