From a5d880832104230ef0568f716fbe5b3dcca5ba0d Mon Sep 17 00:00:00 2001 From: ShaneK Date: Tue, 1 Sep 2026 08:42:11 -0700 Subject: [PATCH 1/4] docs(angular): document app shell change detection requirement on Angular 22 --- docs/angular/zoneless.md | 21 ++++++++++++++++++++- docs/updating/9-0.md | 4 +++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/angular/zoneless.md b/docs/angular/zoneless.md index bbcb5031ab..ce8f87d748 100644 --- a/docs/angular/zoneless.md +++ b/docs/angular/zoneless.md @@ -27,7 +27,7 @@ You do not need to change these. Angular schedules change detection for them in - Navigation, route transitions, and tab switching. :::note[Angular 22] -Angular 22 also makes `OnPush` the default change detection strategy. Under `OnPush`, synchronous state set as a plain field (including in the lifecycle hooks above) no longer re-renders on its own, even though Ionic notifies Angular. Signals still update the view. For the migration path, refer to the [OnPush Change Detection section of the Ionic 9 upgrade guide](/docs/updating/9-0.md#onpush-change-detection-on-angular-22). +Angular 22 also makes `OnPush` the default change detection strategy. Under `OnPush`, synchronous state set as a plain field (including in the lifecycle hooks above) no longer re-renders on its own, even though Ionic notifies Angular. Signals still update the view. Refer to [Change detection on Angular 22](#change-detection-on-angular-22) for what this means for your app shell, and to the [OnPush Change Detection section of the Ionic 9 upgrade guide](/docs/updating/9-0.md#onpush-change-detection-on-angular-22) for the migration steps. ::: ## What needs a notification @@ -149,6 +149,25 @@ export class AppComponent { } ``` +## Change detection on Angular 22 + +On Angular 22 a component that does not declare a strategy is `OnPush`. If your pages keep state in plain fields rather than signals, every component from your application root down to the one hosting `ion-router-outlet` or `ion-tabs` (your app shell) must stay eager. A tick starts at the application root and skips a clean `OnPush` view and everything below it, so an `OnPush` ancestor strands the page even when the page itself is eager: + +```ts +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + changeDetection: ChangeDetectionStrategy.Eager, + template: '', +}) +export class AppComponent {} +``` + +If other components sit between your application root and `ion-router-outlet`, each of them needs the same declaration. Pages that set state through signals, or that call `markForCheck()`, are unaffected: both mark the ancestor chain, so a tick reaches them whatever the shell declares. Converting your pages that way is the alternative to keeping the shell eager. + +Hosting an `ion-nav` is fine either way, because its pages are attached as root views and are checked independently of the component hosting them. + ## Staying on Zone.js If you are not ready to adopt zoneless change detection, you can opt back into Zone.js with `provideZoneChangeDetection()`. Refer to the [Keeping Zone.js section of the Ionic 9 upgrade guide](/docs/updating/9-0.md#keeping-zonejs) for the exact configuration. diff --git a/docs/updating/9-0.md b/docs/updating/9-0.md index b2b5efaf5f..129be8b86c 100644 --- a/docs/updating/9-0.md +++ b/docs/updating/9-0.md @@ -134,8 +134,10 @@ Run `ng update` when upgrading to Angular 22; it migrates your existing componen + } ``` +`ng update` only migrates the components that exist when you run it. From then on `ng generate component` produces `OnPush` components, as does `ng new` for a new app, so a component added later between your application root and `ion-router-outlet` can stop change detection before it reaches your pages. Refer to [Change detection on Angular 22](/docs/angular/zoneless.md#change-detection-on-angular-22) for which components in an Ionic app have to stay eager. + :::note -Ionic's own Angular components already declare `OnPush`, so they are unaffected. Angular 18 through 21 keep the eager default and require no change. +Ionic's own Angular components declare their strategy explicitly, so an Angular version bump can't change it. The `ion-router-outlet` and `ion-tabs` components stay eager so they do not block change detection on its way to your routed pages, and the rest are `OnPush`. Angular 18 through 21 keep the eager default and require no change. ::: #### TypeScript From 7bedf1e16318736f5a2c87b474c0415e761966bc Mon Sep 17 00:00:00 2001 From: ShaneK Date: Wed, 2 Sep 2026 06:39:43 -0700 Subject: [PATCH 2/4] docs(angular): correct OnPush guidance in lifecycle note --- docs/angular/lifecycle.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/angular/lifecycle.md b/docs/angular/lifecycle.md index a26dac3128..d44e40aaa6 100644 --- a/docs/angular/lifecycle.md +++ b/docs/angular/lifecycle.md @@ -28,7 +28,9 @@ For more info on the Angular Component Life Cycle events, visit their [component :::note -Components that use `ion-nav` or `ion-router-outlet` should not use the `OnPush` change detection strategy. Doing so will prevent lifecycle hooks such as `ngOnInit` from firing. Additionally, asynchronous state changes may not render properly. +If your pages keep state in plain fields rather than signals, the component hosting `ion-router-outlet` or `ion-tabs` needs eager change detection, as does every component between it and your application root. A change detection pass starts at the application root and skips a clean `OnPush` view along with everything below it, so an `OnPush` component above the outlet stops updates from reaching the routed pages under it. The pages themselves can use `OnPush`, as long as their state is a signal or they call `markForCheck()`. + +On Angular 18 through 21 this only affects you if you set `OnPush` on those components yourself, because a component that does not declare a strategy is eager. Angular 22 makes `OnPush` the default for components that do not declare one, so refer to [Change detection on Angular 22](/docs/angular/zoneless.md#change-detection-on-angular-22) for what your app shell has to declare. ::: From d8f9d409661f5328105cde958523c6ce0b03b2fa Mon Sep 17 00:00:00 2001 From: ShaneK Date: Wed, 2 Sep 2026 06:55:53 -0700 Subject: [PATCH 3/4] chore(git): reverting change --- docs/updating/9-0.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/updating/9-0.mdx b/docs/updating/9-0.mdx index 668c218ca1..0538192928 100644 --- a/docs/updating/9-0.mdx +++ b/docs/updating/9-0.mdx @@ -142,11 +142,9 @@ Run `ng update` when upgrading to Angular 22; it migrates your existing componen + } ``` -`ng update` only migrates the components that exist when you run it. From then on `ng generate component` produces `OnPush` components, as does `ng new` for a new app, so a component added later between your application root and `ion-router-outlet` can stop change detection before it reaches your pages. Refer to [Change detection on Angular 22](/docs/angular/zoneless.mdx#change-detection-on-angular-22) for which components in an Ionic app have to stay eager. - :::note -Ionic's own Angular components declare their strategy explicitly, so an Angular version bump can't change it. The `ion-router-outlet` and `ion-tabs` components stay eager so they do not block change detection on its way to your routed pages, and the rest are `OnPush`. Angular 18 through 21 keep the eager default and require no change. +Ionic's own Angular components already declare `OnPush`, so they are unaffected. Angular 18 through 21 keep the eager default and require no change. ::: From 438e667bdba44278b2591092c1f3de864a53bd6c Mon Sep 17 00:00:00 2001 From: Shane Date: Wed, 2 Sep 2026 11:19:53 -0700 Subject: [PATCH 4/4] docs(lifecycle): phrasing Co-authored-by: Maria Hutt --- docs/angular/lifecycle.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/angular/lifecycle.mdx b/docs/angular/lifecycle.mdx index 48c0dc90e9..324c78f0a6 100644 --- a/docs/angular/lifecycle.mdx +++ b/docs/angular/lifecycle.mdx @@ -30,7 +30,9 @@ For more info on the Angular Component Life Cycle events, visit their [component If your pages keep state in plain fields rather than signals, the component hosting `ion-router-outlet` or `ion-tabs` needs eager change detection, as does every component between it and your application root. A change detection pass starts at the application root and skips a clean `OnPush` view along with everything below it, so an `OnPush` component above the outlet stops updates from reaching the routed pages under it. The pages themselves can use `OnPush`, as long as their state is a signal or they call `markForCheck()`. -On Angular 18 through 21 this only affects you if you set `OnPush` on those components yourself, because a component that does not declare a strategy is eager. Angular 22 makes `OnPush` the default for components that do not declare one, so refer to [Change detection on Angular 22](/docs/angular/zoneless.mdx#change-detection-on-angular-22) for what your app shell has to declare. +On **Angular 18 through 21** this only affects you if you set `OnPush` on those components yourself, because a component that does not declare a strategy is eager. + +**Angular 22** makes `OnPush` the default for components that do not declare one, so refer to [Change detection on Angular 22](/docs/angular/zoneless.mdx#change-detection-on-angular-22) for what your app shell has to declare. :::