Skip to content

Remove NgZone usage across the project for Angular 21 zoneless compatibility #14035

@droshev

Description

@droshev

Problem

Angular 21 defaults to zoneless change detection. Code that relies on NgZone events like onMicrotaskEmpty and onStable will silently break — these observables never emit in zoneless mode.

We already hit this in the Carousel component, fixed in #14028: buttons and indicators were missing because NgZone.onMicrotaskEmpty never fired.

The same class of bug exists in 27 other files across the project.

High Risk: onMicrotaskEmpty / onStable subscribers (17 files)

These will break in zoneless mode — the subscriptions never fire, causing missing UI, broken initialization, or stale state.

# File Usage Line(s) Assignee PR
1 libs/cdk/utils/directives/initial-focus/initial-focus.directive.ts onMicrotaskEmpty.pipe(take(1)) :74 Inna #14044
2 libs/cdk/utils/directives/disabled/fdk-disabled-provider.service.ts firstValueFrom(ngZone.onStable) :70 Maria
3 libs/cdk/utils/directives/readonly/fdk-readonly-provider.service.ts firstValueFrom(ngZone.onStable) :75 Maria
4 libs/core/tabs/tab-list.component.ts onStable + onMicrotaskEmpty :379, :384 Deno #14042
5 libs/core/tabs/tab-panel/tab-panel.component.ts onStable.pipe(first()) :160 Deno #14042
6 libs/core/form/form-item/form-item.component.ts onStable.pipe(first()) :63 Inna #14045
7 libs/core/table/table.component.ts onStable.pipe(first()) :141 Mike
8 libs/core/timeline/components/timeline-node-body/timeline-node-body.component.ts onStable.pipe(first()) :36 Maria
9 libs/core/menu/directives/segmented-button/segmented-button-option.directive.ts delayWhen(() => ngZone.onStable) :58 Deno #14043
10 libs/core/user-menu/components/user-menu-list-item.component.ts onStable.pipe(startWith(isStable)) :306 Inna #14046
11 libs/btp/tool-header/src/components/tool-header/tool-header.component.ts onStable (delayWhen + first()) :193, :250 Maria
12 libs/btp/navigation/components/navigation-start/navigation-content-start.component.ts onStable.pipe(startWith(isStable)) :139 Deno #14049
13 libs/btp/navigation/components/navigation-item/navigation-list-item.component.ts onStable.pipe(startWith(isStable)) :963 Inna #14047
14 libs/platform/icon-tab-bar/components/icon-tab-bar-base.class.ts onMicrotaskEmpty.pipe(take(1)) :299 Deno #14051
15 libs/platform/icon-tab-bar/components/icon-tab-bar-process-type/icon-tab-bar-process-type.component.ts onMicrotaskEmpty.pipe(take(1)) :107 Deno #14051
16 libs/platform/table/table.component.ts onMicrotaskEmpty.pipe(take(1)) :2391 Mike
17 libs/docs/platform/form-generator/examples/platform-form-generator-inline-help-example.component.ts onStable.pipe(first()) :102 Inna #14048

Low Risk: runOutsideAngular / run (12 files)

These won't break (they're effectively no-ops in zoneless), but they are dead code that should be cleaned up:

# File Usage Line(s) Assignee PR
1 libs/cdk/utils/directives/auto-complete/auto-complete.directive.ts runOutsideAngular :78 Maria #14081
2 libs/cdk/utils/directives/overflow-list/overflow-list.directive.ts run() :115, :122 Maria #14079
3 libs/btp/splitter/splitter-resizer/splitter-resizer.component.ts runOutsideAngular + run :188, :192, :203 Inna #14052
4 libs/btp/tool-header/src/components/tool-header/tool-header.component.ts runOutsideAngular :275 Maria #14076
5 libs/core/segmented-button/segmented-button.component.ts run() :233 Deno #14053
6 libs/platform/form/auto-complete/auto-complete.directive.ts runOutsideAngular :86 Inna #14054
7 libs/platform/table-helpers/directives/table-header-resizer.directive.ts runOutsideAngular :59 Mike
8 libs/platform/table-helpers/directives/table-cell-resizable.directive.ts runOutsideAngular :72 Mike
9 libs/platform/table-helpers/directives/table-scrollable.directive.ts runOutsideAngular :53, :198 Mike
10 libs/platform/table/components/table-column-resizer/table-column-resizer.component.ts runOutsideAngular :58, :102 Mike
11 libs/platform/table/table.component.ts runOutsideAngular :2345 Mike
12 libs/platform/table/components/table-row/table-row.component.ts runOutsideAngular :250 Mike

Test file to update

  • libs/platform/icon-tab-bar/components/icon-tab-bar-process-type/icon-tab-bar-process-type.component.spec.ts — mocks NgZone, needs updating alongside its component — Deno (same as component Code formatting using Prettier JS #15)

Suggested migration patterns

Based on the approach used in #14028 (carousel fix):

Current pattern Replacement
onMicrotaskEmpty.pipe(take(1)).subscribe(fn) queueMicrotask(fn)
onStable.pipe(first()).subscribe(fn) afterNextRender(fn) or queueMicrotask(fn)
firstValueFrom(ngZone.onStable).then(fn) queueMicrotask(fn) or Promise.resolve().then(fn)
delayWhen(() => ngZone.onStable) delay(0) or restructure with signals
runOutsideAngular(() => ...) Remove wrapper, call directly
ngZone.run(() => ...) Remove wrapper, call directly

After each migration, remove the NgZone import and injection.

Summary per developer

Developer High Risk Low Risk Total
Mike 2 (core/table, platform/table) 6 (all table-related) 8
Maria 3 (disabled-provider, readonly-provider, timeline, tool-header) 3 (overflow-list, tool-header, auto-complete) 6
Inna 4 (initial-focus, form-item, user-menu, navigation-list-item, form-generator) 2 (splitter-resizer, auto-complete) 6
Deno 5 (tab-list, tab-panel, segmented-button-option, navigation-start, icon-tab-bar-process) 1 (segmented-button) + test file 6

Priority

The 17 high-risk files should be addressed first — they will cause the same silent failures as the carousel bug (missing UI elements, broken initialization).

References

Related issues

This is the parent tracking issue for Angular 21 zoneless migration. The following sub-issues cover additional areas that need attention:

Suggested order of work

  1. Remove NgZone usage across the project for Angular 21 zoneless compatibility #14035 (this issue) — Remove all NgZone usages first (high-risk items are blocking)
  2. Audit setTimeout/setInterval usage for zoneless compatibility #14037 — Audit setTimeout/setInterval call sites
  3. Audit ChangeDetectionStrategy.Default components for zoneless compatibility #14039 — Migrate Default strategy components to OnPush
  4. Remove zone.js polyfill and configure zoneless bootstrap for Angular 21 #14038 — Remove zone.js and enable zoneless bootstrap (only after 1-3 are done)

Metadata

Metadata

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions