Mert Özen Further With Every Line
Frontend Angular — #31

Angular 22: OnPush by Default, Stable Signal Forms and a File-by-File Comparison with Angular 12

Mert Özen Oct 31, 2026 7 min 5 views
Angular 22: OnPush by Default, Stable Signal Forms and a File-by-File Comparison with Angular 12

In Angular 22 OnPush is the default, Signal Forms and the resource APIs are stable, and HttpClient uses Fetch. We also cover the yearly major cadence and a file-by-file diff with an Angular 12 project.

Version note: This post covers what Angular 22 introduced; the code targets Angular 22.

Angular 22: The Payoff of a Long Preparation

In Angular 21 we saw zoneless become the default, Signal Forms arrive as an experiment, and Vitest take Karma's place. Angular 22, released on June 3, 2026, completes the groundwork laid over the last few versions: Signal Forms and the resource APIs become stable, OnPush becomes the default change detection strategy, and HttpClient switches to Fetch under the hood.

This post is a bit longer than the others. After the new features, we will compare a project created with Angular 12 against one created today with ng new, file by file. Nothing shows the distance between where this series started and where we are today better than that comparison.

OnPush Is Now the Default

In Angular 12, a task list component and the parent using it could look like this:

@Component({
  selector: 'app-task-list',
  template: `<li *ngFor="let task of tasks">{{ task }}</li>`
})
export class TaskListComponent {
  @Input() tasks: string[] = [];
}

// AppComponent
tasks = ['Market alışverişi'];

addTask(): void {
  this.tasks.push('Fatura öde');
}

Clicking the button in the parent added an item to the array and the list grew on screen. The default strategy checked the whole tree every time, so it didn't matter that the array reference stayed the same. When we covered change detection earlier, OnPush was something we had to switch on separately and deliberately.

Let's write the same code in Angular 22 with the new syntax:

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-task-list',
  template: `
    @for (task of tasks(); track $index) {
      <li>{{ task }}</li>
    }
  `
})
export class TaskList {
  readonly tasks = input<string[]>([]);
}

There is no changeDetection in the decorator, yet this component is OnPush. In Angular 22, every component that doesn't specify a strategy behaves as OnPush. If the parent still mutates the same array with push, the list won't grow on screen: the reference passed to the input stays the same, so Angular sees no reason to check this component.

The fix is to make the change in a way Angular can see:

export class App {
  protected readonly tasks = signal(['Market alışverişi']);

  protected addTask(): void {
    this.tasks.update(list => [...list, 'Fatura öde']);
  }
}

In the parent template you write [tasks]="tasks()". Each addition creates a new array, the signal changes, and the child updates. If a component needs the old behavior, you say so explicitly:

@Component({
  selector: 'app-task-list',
  changeDetection: ChangeDetectionStrategy.Eager,
  template: `...`
})

Now you can see more clearly why Default was renamed to Eager in the previous version: it is no longer the default. The migration that runs during ng update adds ChangeDetectionStrategy.Eager to existing components that don't specify a strategy, so your app keeps working the same way after the upgrade.

Signal Forms and the Resource APIs Are Stable

Signal Forms, which we met as an experiment in Angular 21, became stable with Angular 22. The final shape matches the 21.1 rename:

import { Component, signal } from '@angular/core';
import { form, FormField, required, email } from '@angular/forms/signals';

@Component({
  selector: 'app-login',
  imports: [FormField],
  template: `
    <input type="email" [formField]="loginForm.email" />
    @if (loginForm.email().touched() && loginForm.email().invalid()) {
      <p>Geçerli bir e-posta gir</p>
    }
  `
})
export class Login {
  private readonly credentials = signal({ email: '', password: '' });
  protected readonly loginForm = form(this.credentials, (f) => {
    required(f.email, { message: 'E-posta zorunlu' });
    email(f.email);
  });
}

Stable means a promise that the API won't be renamed from one version to the next anymore. Alongside that, Angular 22 added things like the minDate/maxDate validators, debounce() to delay a field's validation, and validateHttp() for validations that ask the server.

The resource family got the same promise. resource({ params, loader }), rxResource({ params, stream }) and httpResource(() => url), experimental since Angular 19, are now stable. If you followed the renames in 19 and 20, the last shape you saw is the one that stays.

HttpClient Now Uses Fetch

In Angular 12, using HttpClient meant adding it to a module first:

@NgModule({
  imports: [BrowserModule, HttpClientModule],
  // ...
})
export class AppModule { }

Since Angular 21, HttpClient has been available in root out of the box. What changes in Angular 22 is the engine underneath: HttpClient now sends requests with the browser's Fetch API instead of XMLHttpRequest. withFetch(), previously recommended for SSR, is now the default and therefore deprecated.

Most of the time nothing changes on your side; get, post and interceptors are the same. The one thing Fetch doesn't support is upload progress. If you have a screen that shows an upload percentage, you ask for the old engine explicitly:

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withXhr())]
};

That is also why the reportProgress option was deprecated, replaced by reportUploadProgress and reportDownloadProgress. If your project has provideHttpClient(), the update migration adds withXhr() so behavior doesn't change.

Compiler and Build: strictTemplates and Webpack

An Angular 12 project's tsconfig.json had these lines:

"angularCompilerOptions": {
  "enableI18nLegacyMessageIdFormat": false,
  "strictInjectionParameters": true,
  "strictInputAccessModifiers": true,
  "strictTemplates": true
}

A project created with Angular 22 has no strictTemplates line, because it is now on by default. TypeScript 6 also turns strict on by default, so the "strict": true line disappeared too. The lines are gone, but the strict checks are still there. If an existing project has this setting off, the migration adds "strictTemplates": false so behavior doesn't change.

An era is closing on the build side as well. In Angular 12 the builder in angular.json was @angular-devkit/build-angular:browser, with webpack running behind it. Since Angular 17, new projects have used the esbuild-based application builder; today it comes from a separate package as @angular/build:application. With Angular 22, the webpack-based @angular-devkit/build-angular builders are deprecated, and the Jest and Web Test Runner builders are removed entirely. If you still have a project building with webpack, now is the time to plan the move.

An Angular 12 Project vs an Angular 22 Project, File by File

The skeleton of the project we created with ng new in Angular 12:

ilk-uygulama/
  .browserslistrc
  .editorconfig
  .gitignore
  angular.json
  karma.conf.js
  package.json
  README.md
  tsconfig.json
  tsconfig.app.json
  tsconfig.spec.json
  src/
    favicon.ico
    index.html
    main.ts
    polyfills.ts
    styles.css
    test.ts
    assets/
    environments/
      environment.ts
      environment.prod.ts
    app/
      app.module.ts
      app.component.ts
      app.component.html
      app.component.css
      app.component.spec.ts

The same command in Angular 22 produces:

ilk-uygulama/
  .editorconfig
  .gitignore
  .prettierrc
  .vscode/
  angular.json
  package.json
  README.md
  tsconfig.json
  tsconfig.app.json
  tsconfig.spec.json
  public/
    favicon.ico
  src/
    index.html
    main.ts
    styles.css
    app/
      app.ts
      app.html
      app.css
      app.spec.ts
      app.config.ts
      app.routes.ts

The biggest difference is in src/app. There is no app.module.ts; app-wide providers live in app.config.ts. In Angular 12, choosing routing gave you a separate app-routing.module.ts; now routing is the default and routes are a plain array in app.routes.ts. The root component lives in app.ts instead of app.component.ts, and it is called App.

main.ts tells the whole story at a glance:

// Angular 12
if (environment.production) {
  enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

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

Each missing file has a story. polyfills.ts is gone because there is no Zone.js left to load. karma.conf.js and test.ts are gone because Vitest runs the tests. The environments/ folder and .browserslistrc haven't been generated by default since Angular 15. src/assets gave way to the public/ folder at the root. The newcomers are .prettierrc and the .vscode/ folder with VS Code settings.

package.json tells the same story. Angular 12 had zone.js ~0.11.4, typescript ~4.3.5 and a dozen Karma/Jasmine packages. Angular 22 has no zone.js at all; the dev dependencies list @angular/build, vitest, jsdom, prettier and typescript ~6.0.2. The generated app.ts says neither standalone nor changeDetection; both are defaults now.

One Major Release a Year

From Angular 12 to 22, a new major version arrived every six months; that is why each post in this series lined up with a version. With Angular 22 the schedule changed: there is now one major release a year, with four to six minor releases in between. According to the plan, 22.3 is expected in November 2026, and Angular 23 around mid-2027, roughly in June.

Support was adjusted to match: each major version gets 12 months of active support followed by 12 months of LTS. A practical note: if you are on Angular 20, its LTS ends on November 28, 2026, so don't put off your upgrade plan.

Moving to Angular 22: ng update

ng update @angular/core@22 @angular/cli@22

Angular 22 requires TypeScript 6.0; older versions, including 5.9, are not supported. Node.js 20 support is gone; you need one of the ^22.22.3, ^24.15.0 or ^26.0.0 lines. If you want to bring an Angular 12 project all the way here, remember that the CLI won't let you skip more than one major version at a time: you have to go 13, 14, 15 and so on, one by one.

The migrations in this update do quite a bit of work: they add Eager to components, and add withXhr() and "strictTemplates": false where needed. The ?. operator in templates now returns undefined like it does in TypeScript; the migration protects expressions that depend on the old behavior by wrapping them in a special function. Once the command finishes, read the git diff output carefully.

A Small Experiment

Create a project with npx @angular/cli@22 new deneme. Notice that the CLI no longer asks about routing or zoneless, and asks about styles, SSR and AI tools instead. Once the project is created, compare its folder structure with the Angular 22 tree above.

Then build the TaskList example. In the parent, first use a plain array with push, click the button and see that the list doesn't grow. Next, add changeDetection: ChangeDetectionStrategy.Eager to the child and try again. Finally, remove Eager and rewrite the parent to use a signal with update. Seeing the three cases side by side is the shortest path to making OnPush click.

The series doesn't end here. If the schedule holds, Angular 23 arrives in mid-2027, and as new versions come out we will keep explaining what changed the same way, side by side with how it looked in Angular 12.