Old → new cheatsheet
Every substitution from the 2022 way of writing Angular to the 2026 way, grouped by area. Keep this one open while you migrate.
Each line reads what you used to write → what you write now. Where the change is more than syntax, the topic that explains it is linked from the heading.
One computed over two inputs
The query and the topic both feed the same computed, so the table and the row count can never disagree. Type signal, or *ngIf, or pick a topic.
| Before | Now | Topic |
|---|---|---|
| *ngIf / *ngFor | @if / @for | Templates |
| ngClass / ngStyle | [class.x] / [style.x] | Templates |
| structural directive microsyntax | @switch / @let | Templates |
| NgModules | standalone components | Components |
| @Input() / @Output() | input() / output() | Components |
| constructor injection | inject() | DI |
| classes with providedIn | @Service() with a scope | DI |
| HttpModule + map(res => res.json()) | provideHttpClient + httpResource() | HTTP |
| subscribe() in ngOnInit | resource() / signals | HTTP |
| RouterModule.forRoot | provideRouter(routes) | Routing |
| ActivatedRoute.snapshot.params | withComponentInputBinding() | Routing |
| class guards and resolvers | CanMatchFn and ResolveFn functions | Routing |
| new FormControl + Validators.x | form() + field state | Forms |
| zone.js patching everything | provideZonelessChangeDetection() | Runtime |
| OnPush written on every component | on by default, signals drive it | Runtime |
| prerender as an add-on | outputMode: static + hydration | Runtime |
State & reactivity
See Signals.
new BehaviorSubject<T>(initial)→signal<T>(initial).next(value)→.set(value)/.update(fn).getValue()/.value→ call it:count()combineLatest([...]).pipe(map(...))for derived state →computed(() => ...){{ stream$ | async }}→{{ value() }}- A field plus
ngOnChangesto derive it →computed()over the input signal - A field plus
subscribeto keep it in sync →linkedSignal() toSignal(observable$)— still right — for streams you want as stateeffect()to keep state in sync →computed(derived) orlinkedSignal(writable)effect()for DOM access after render →afterRenderEffect()
Components & templates
See Standalone components, The component contract and Templates & control flow.
@NgModule+declarations→importson the component that needs itstandalone: true→ the default; the flag is going awayplatformBrowserDynamic().bootstrapModule(AppModule)→bootstrapApplication(App, appConfig)providers: [...]in a module →providersinapp.config.ts, or on a route/component@Input() name!: string→name = input.required<string>()@Input() name = 'x'→name = input('x')@Output() saved = new EventEmitter<T>()→saved = output<T>()@Input()+@Output() xChange→x = model<T>()@ViewChild('el') el!: ElementRef→el = viewChild.required<ElementRef>('el')@ViewChildren(X)→viewChildren(X)@ContentChild(X)→contentChild(X)*ngIf="cond"→@if (cond) { }*ngIf="a; else b"+ng-template→@if (a) { } @else { }*ngFor="let x of xs; trackBy: fn"→@for (x of xs; track x.id) { }*ngIf="xs.length"around a*ngFor→@empty { }on the@for[ngSwitch]+*ngSwitchCase→@switch/@case/@default- An
ng-containerto hold a local value →@let CommonModuleimported for the basics → nothing; control flow is syntax, pipes import singly- A method call in the template →
computed()
Dependency injection
See Dependency injection.
constructor(private api: Api) {}→private readonly api = inject(Api);@Injectable({ providedIn: 'root' })→@Service()APP_INITIALIZERmulti-provider →provideAppInitializer(() => ...)- Constructor-only injection (guards, resolvers, factories) →
inject()anywhere in an injection context - A lazily loaded dependency inside a constructor →
injectAsync() { provide: X, useClass: Y }— unchanged, still the seam for tests
Routing
See Routing.
RouterModule.forRoot(routes)→provideRouter(routes, ...features)RouterModule.forChild(routes)in a feature module →loadChildren: () => import('./x.routes').then((m) => m.X_ROUTES)- A lazy module →
loadComponent: () => import('./page').then((m) => m.Page) Title.setTitle()in a component →titleon the route (static or a function)- A class
CanActivateguard registered in providers → aCanActivateFnin the route array ActivatedRoute.paramMap.subscribe(...)→withComponentInputBinding()+slug = input.required<string>()- Navigate imperatively in a guard → return a
UrlTree - Nothing (transitions came from CSS) →
withViewTransitions()
Data & forms
See HTTP & async data and Forms.
HttpClientModule→provideHttpClient(withFetch())- A class
HttpInterceptor+HTTP_INTERCEPTORS→ anHttpInterceptorFn+withInterceptors([...]) subscribe()+ aloadingboolean + anerrorfield →resource({ params, loader })switchMapon a param stream to refetch →resource({ params: () => this.query() })- A GET whose URL comes from signals →
httpResource<T>(() => \/api?q=${this.query()}`)` loading: truein the template →@if (res.isLoading())- Manual retry wiring →
res.reload() new FormGroup({...})→ still supported; orform(this.model, (path) => {...})for Signal FormsValidators.required→required(path.x, { message })form.valueChanges.subscribe(...)→ read the field signal:signup.email().value()form.statusChangesto drive the submit button →signup().invalid()
Change detection & runtime
See Zoneless change detection, Performance and SSR & hydration.
zone.jspolyfill inangular.json→ nothing; zoneless is the defaultprovideZoneChangeDetection()→provideZonelessChangeDetection()(usually implicit)changeDetection: ChangeDetectionStrategy.OnPushon every component → the default for new components- A plain field updated from
setTimeout/websockets → asignal markForCheck()/detectChanges()→ make the value a signalDetectChangesconfusion in dev →provideCheckNoChangesConfig({ exhaustive: true })@angular/platform-server+ manual prerender wiring →ng add @angular/ssr+outputMode/routesFile- Re-rendering on the client after SSR →
provideClientHydration(withEventReplay()) - Losing clicks before hydration finishes → event replay (above)
isPlatformBrowsereverywhere →afterNextRender()where the work is not render-related
Build & CLI
- Webpack build → esbuild + Vite (the CLI's own pipeline)
ng build --prod→ng build(production is the default configuration)ng lintwith TSLint → ESLint viang add @angular/eslint- A hand-rolled "did I break the bundle" check → build budgets in
angular.json - Manual module conversion →
ng generate @angular/core:standalone - Manual
*ngIfrewrite →ng generate @angular/core:control-flow
The three rules behind all of it
If a substitution above is not obvious, it is probably one of these:
- Read state where you use it, write it where it changes. Signals make the dependency explicit, which is what the rest of the framework now leans on — see Signals.
- Derive, do not synchronise. If a value follows from other state, it is a
computed; if it must also be writable, it is alinkedSignal. - Let the framework decide when to render. No nudges, no
tick(), nosetTimeoutto wait for the DOM — see Zoneless change detection.
And if you want the story in reading order rather than as a lookup table, start at Orientation.