Angular PrimerAngular 22 · 2022 → 2026

Un-learn & upgrade

The patterns that were right in 2022 and are wrong now, the error each one produces, and the order to do the migration in.

The un-learn list

Each entry pairs the old habit with the sign you are hitting it, so you can work backwards from a failing build.

  1. State in a BehaviorSubject. Symptom: a template that still needs | async, and cleanup code in ngOnDestroy. Replace with signal() — see Signals.
  2. *ngIf / *ngFor and a CommonModule import. Symptom: a template that reads like a directive soup, and a trackBy function you have to remember. Use @if / @for with track — see Templates & control flow.
  3. ngOnChanges to derive from an input. Symptom: the derived value is one frame behind on the first render. Use computed() over the input signal — see The component contract.
  4. ChangeDetectorRef.markForCheck() / detectChanges(). Symptom: a nudge call that only exists because a value outside the signal graph drives the template. Make it a signal — see Zoneless change detection.
  5. Constructor injection with private readonly parameters. Not wrong, but it cannot be used in guards, resolvers or factories. Use inject() — see Dependency injection.
  6. @Injectable({ providedIn: 'root' }) on everything. Fine, but @Service() is the v22 one-liner, and narrower providers are usually what you actually wanted.
  7. subscribe() in a component to fill a field. Symptom: a Subscription field and an ngOnDestroy, or a leak. Use resource() / httpResource() / toSignal() — see HTTP & async data.
  8. onload="this.media='all'" style workarounds for CSS. Symptom: a stylesheet that loads but does not apply — which, under a strict CSP that blocks inline scripts, is exactly what happens. Turn off Angular's critical-CSS inlining instead; this site does.
  9. A SharedModule that exports everything. Symptom: deleting one import breaks three unrelated components. Import per component — see Standalone components.
  10. setTimeout inside a component to wait for the DOM. Use afterNextRender() or afterRenderEffect().

The errors you will actually see

Error What it means Where to look
NG8113: 'X' is not used within the template An import was added but the template does not use it — common after the standalone migration Delete the import
'app-thing' is not a known element The component using it does not import it (there is no ambient module any more) Add it to imports
inject() must be called from an injection context inject() inside a callback, a setTimeout, or after an await Move it to a field, or use runInInjectionContext
NG0100: Expression has changed after it was checked State was written during change detection Derive with computed instead of writing state
NG0500: hydration mismatch The browser's first render differs from the prerendered HTML Remove Date.now()/localStorage from render paths
Cannot match any routes A routerLink or navigate path does not exist Check the route table, including the ** entry
Type 'Signal<T>' is not assignable to ... A signal was passed where its value was expected Read it: count()

Upgrading

The mechanics have not changed, and the discipline around them matters more than the commands:

ng update                       # what is outdated, and what the CLI recommends
ng update @angular/core @angular/cli
ng update @angular/core@22 @angular/cli@22   # pin the next major explicitly
  • Never --force past a peer-dependency error to make an upgrade "work" — it usually means a third-party library has not caught up, and you will find out at runtime.
  • Read the update guide for each major. Angular's update.angular.io lists the breaking changes between your version and the target, and the migrations that automate them (ng update runs schematics such as the standalone conversion and the control-flow rewrite).
  • Upgrade one major at a time, on a branch, with the tests green between steps. Angular supports skipping majors, but a bisectable history is worth more than a single afternoon.
  • Convert to standalone before converting to signals. The standalone schematic is mechanical and mostly safe; the signals conversion changes how your components behave, and mixing the two in one branch makes both harder to review.

A pragmatic order for a real application:

  1. ng update to the target version, fix compile errors, ship it. No behaviour change.
  2. ng generate @angular/core:standalone, prune the modules, ship it.
  3. Convert state to signals component by component, starting with the components that have the most manual change-detection nudges.
  4. Convert templates to the new control flow (ng generate @angular/core:control-flow).
  5. Remove zone.js last, once provideCheckNoChangesConfig({ exhaustive: true }) is quiet in development.

CLI cheatsheet

ng new app --style=tailwind --zoneless          # new project, current defaults
ng generate component pages/topic --skip-tests  # scaffold
ng generate @angular/core:standalone            # Module → standalone migration
ng generate @angular/core:control-flow          # *ngIf → @if migration
ng generate @angular/core:signals               # introduce signal inputs
ng build                                        # production bundle
ng serve                                        # dev server (esbuild + Vite, HMR)
ng test                                         # unit tests
ng update                                       # upgrade guidance
ng add @angular/ssr                             # add prerendering / SSR

Every ng generate takes --dry-run; use it on the migrations before you let them write.

The failure mode to watch for during a migration is not a build error, it is a silently stale view: a value updated outside the signal graph still renders in development because a click handler happened to trigger a check, and stops rendering once the value changes from a timer or a socket. That is the pattern in item 4 and 8 above, and it is why the exhaustive check configuration — not the test suite — is what tells you the migration is finished.

What to take away

Almost nothing you know is wasted: components, DI, routing and forms all still mean what they meant. The migration is mostly deleting bookkeeping that the framework no longer needs — and the bookkeeping is exactly what the Old → new cheatsheet lists, side by side.