Skip to main content

no-zone-run-apis

Legacy guidance

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 NoopNgZone

In 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.