The Angular team published an RFC to make OnPush the default CD (change detection) Strategy: [https://github.com/angular/angular/discussions/66779](https://github.com/angular/angular/discussions/66779). This change will be available in Angular v22.

Angular has always been a "magic-based" framework for a long time when it comes to change detection. That's because of zone.js handling most of the async operations and notifying Angular that something has changed.

```
@Component({
  template: ` 
     <button (click)="onClick()">{{ count }}</button> 
  `,
})
export class AsyncClick {
  count = 0;
  onClick() {
    setTimeout(() =&gt; {
      this.count++;
    }, 1000);
  }
}
```

The last version of Angular (v21) removed that magic for new projects by making Zoneless the default choice. This means that the code above would not work as expected.

The reason for this is because nothing is listening to that `setTimeout` callback being executed. While the view still updates, it's not correct.

> The reason it still updates is because when we click a button Angular triggers a change detection run (same as calling `markForCheck` manually). But it's one update cycle too late.

This means that we shouldn't depend anymore on zone.js notifying Angular that something has changed. We need to be explicit when we change state that shows something in the browser.

That's what signals solve! By using signals, we can tell Angular what exactly changed and the framework will take care of the rest.

```
@Component({
  template: ` 
    <button (click)="onClick()">{{ count() }}</button> 
  `,
})
export class AsyncClick {
  count = signal(0);  onClick() {
    setTimeout(() =&gt; {
      this.count.update((x) =&gt; x + 1);
    }, 1000);
  }
}
```

This now will work as expected. We can see the count updating in the browser as soon as the `setTimeout` callback is executed.

## What about OnPush change detection strategy? Why does it matter when we have signals?

OnPush is what really makes Angular fast. Because it's the only way to skip change detection for a component and its children if they are not dirty (don't have changes). Signals just tell Angular what changed, while OnPush tells Angular what to skip (if not changed explicitly -> don't check).

When we update a signal that is used in the template, Angular will mark the component as dirty (`RefreshView`) and all its ancestors as well (using another flag `HasChildViewsToRefresh`). At the same time, a change detection run will be scheduled to run (checking bindings and refreshing the views if needed). Check more about this in this video: [https://www.youtube.com/watch?v=FvNXnBdIX1M](https://www.youtube.com/watch?v=FvNXnBdIX1M)

Because Angular until now has used a `ChangeDetectionStrategy.Default` strategy, it was not possible to skip change detection for a component and its children if they are not dirty.

> That's one of the reason why `ChangeDetectionStrategy.Default` will be renamed to `ChangeDetectionStrategy.Eager` (meaning that change detection will be run eagerly)

## How to properly migrate to OnPush?

### Set the change detection strategy to OnPush in the component

```
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Component {}
```

### Convert state to use signals

```
@Component({
  template: `
    <button (click)="loadUserData()">Load User Data</button>
    <div>{{ userData()?.name }}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Component {
   private userService = inject(UserService);

-  userData?: UserData | undefined;
+  readonly userData = signal<userdata | undefined>(undefined);

loadUserData() {
    this.userService.getUserData().pipe(take(1)).subscribe((data) =&gt; {
-      this.userData = data;
+      this.userData.set(data);
    });
  }
}</userdata>
```

### Update signal state immutably

```
const userData = signal<userdata | undefined>(undefined);

// ❌ This will not work as expected
userData().name = "John Doe";

// ✅ This is the correct way to update the signal state immutably
userData.update((x) =&gt; ({ ...x, name: "John Doe" }));</userdata>
```

**A little bit of explanation about why direct mutation currently works:**

```
@Component({
  template: `
    <div>{{ userData()?.name }}</div>
    <button (click)="updateName()">Update Name</button>
  `,
})
class Component {
  private userData = signal<userdata | undefined>(undefined);

  updateName() {
    this.userData().name = "John Doe";
  }
}</userdata>
```

The reason why this works is because event listeners still mark the component as dirty. And because the component is dirty and the state is mutated, Angular will run CD and update the view. So, even though it is working, it's not the correct way to do it. Because same as before, if we put it inside a `setTimeout`, the view won't update as expected.

```
@Component({
  template: `
    <div>{{ userData()?.name }}</div>
    <button (click)="updateName()">Update Name</button>
  `,
})
class Component {
  private userData = signal<userdata | undefined>(undefined);
  updateName() {
    setTimeout(() =&gt; {
      this.userData().name = "John Doe";
    }, 1000);
  }
}</userdata>
```

### Recommandation: mark your signals readonly

By marking the signals as readonly, you will get a type error if you try to mutate the signal state directly.

```
const userData = signal<userdata | undefined>(undefined);

// ❌ This will throw a type error
userData = { name: "John Doe";}

// ✅ This is the correct way to update the signal state immutably
userData.update((x) =&gt; ({ ...x, name: "John Doe" }));</userdata>
```

I'm also working to get these as linting errors here: [https://github.com/angular-eslint/angular-eslint/pull/2890](https://github.com/angular-eslint/angular-eslint/pull/2890)

## Angular v22 - Auto Migrations

When you will run `ng update` to update the Angular version to v22, Angular will automatically migrate your project.

1.  It will add `ChangeDetectionStrategy.Eager` to components without explicit change detection
    
    ```
    @Component({
      template: ''
    + changeDetection: ChangeDetectionStrategy.Eager
    })
    export class Cmp {}
    ```
    
2.  It will rename `ChangeDetectionStrategy.Default` to `ChangeDetectionStrategy.Eager`
    
    ```
    @Component({
      template: ''
    - changeDetection: ChangeDetectionStrategy.Default
    + changeDetection: ChangeDetectionStrategy.Eager
    })
    export class Cmp {}
    ```
    
3.  It will keep ChangeDetectionStrategy.OnPush unchanged
    
    ```
    @Component({
      template: ''
      // no changes here
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class Cmp {}
    ```
    

After you migrate to v22, you can safely remove the line that sets OnPush change detection, as that would be the default.

**Angular keeps getting better!**
