no-zone-run-apis
Legacy guidance — since Angular v21, change detection is zoneless by default and Zone.js is dropped from the default bundle. This page documents a legacy approach; Zone.js only may still need it. For new work, prefer the modern approach. See Zoneless & how Zone.js affected change detection for the full picture.
This rule only does anything when Zone.js is present. Since Angular v21, change detection is zoneless by default and Zone.js is dropped from the default bundle, so there is nothing to patch and this rule is a no-op. See Zoneless & how Zone.js affected change detection.
What it flags
NgZone control-flow calls (run, runGuarded, runTask, and runOutsideAngular)
on an injected NgZone. run/runGuarded/runTask re-enter the Angular zone and
trigger a change-detection cycle across the whole app; runOutsideAngular adds
overhead and is easy to misuse. The rule only reports when NgZone is imported.
Options
This rule has no options (schema: []).
// eslintrc
{
"rules": {
"@rx-angular/no-zone-run-apis": "error",
},
}
// eslint.config.js (flat config)
import rxAngular from '@rx-angular/eslint-plugin';
export default [
{
plugins: { '@rx-angular': rxAngular },
rules: {
'@rx-angular/no-zone-run-apis': 'error',
},
},
];
Incorrect
// ❌ Incorrect — NgZone.run / runOutsideAngular steer Zone-based change detection
import { Component, NgZone } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectFoo } from '../../store/foo/foo.selectors';
@Component({
templateUrl: './foo.component.html',
})
export class FooComponent {
constructor(
private zone: NgZone,
private store: Store,
) {
setTimeout(() => {
this.zone.runOutsideAngular(() => {
// ...
});
}, 500);
}
handleEvent(event: any) {
this.store.select(selectFoo).subscribe((value) => {
this.zone.run(() => {
// ...
});
});
}
}
Correct
// ✅ Correct — in a zoneless app (v21+) there is no zone to enter or leave, so the
// NgZone.run* calls are removed entirely. Drive the view with signals (which mark
// the component automatically), or markForCheck() for the rare manual OnPush case.
import { ChangeDetectorRef, Component, inject, signal } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectFoo } from '../../store/foo/foo.selectors';
@Component({
templateUrl: './foo.component.html',
})
export class FooComponent {
private readonly store = inject(Store);
private readonly cdRef = inject(ChangeDetectorRef);
readonly foo = signal<Foo | undefined>(undefined);
handleEvent(event: Event) {
this.store.select(selectFoo).subscribe((value) => {
this.foo.set(value); // signal write schedules CD; no zone.run needed
});
}
}
NgZone.run* no-ops under NoopNgZoneIn a zoneless app Angular provides NoopNgZone, so run, runGuarded, runTask,
and runOutsideAngular do nothing useful: they only invoke the callback. The fix is
to remove the calls, not swap them for another wrapper, and let signals (or
markForCheck) drive change detection.
Why
NgZone run APIs steer Zone-based change detection and can trigger whole-app cycles.
Why → Zoneless & how Zone.js affected change detection.