Skip to main content

RxPush

Legacy guidance

Legacy guidance — since Angular v21, change detection is zoneless by default and AsyncPipe works without Zone.js. This page documents a legacy approach; Zone.js / <21 / per-binding scheduling may still need it. For new work, prefer the modern approach. See Zoneless & how Zone.js affected change detection for the full picture.

Under zoneless change detection (default since Angular v21) the standard AsyncPipe works: it calls markForCheck on every emission, so it is a first-class zoneless notifier. toSignal() is the other native option. Reach for those first. RxPush is worth keeping only for its residual unique value, the per-binding render scheduling described below.

RxPush (the push pipe) binds an Observable or Promise to the view the way AsyncPipe does, but routes the resulting change detection through a prioritized, cancelable render strategy instead of marking the whole component tree dirty.

Residual unique value: per-binding render scheduling

AsyncPipe marks the component and all its ancestors dirty on each emission and lets Angular flush them together. RxPush instead schedules each binding's change detection independently through the render-strategy scheduler, so a batch of high-frequency emissions can be coalesced and spread across frames rather than blocking a single synchronous flush. That per-binding, frame-budgeted scheduling, not any Zone.js concern, is the reason to prefer it over AsyncPipe/toSignal() on modern Angular.

For the scheduling model this builds on, see Concurrent scheduling & the frame budget.

Import

import { RxPush } from '@rx-angular/template/push';

RxPush is a standalone pipe (name: 'push'); add it to a component's imports.

Usage

import { Component } from '@angular/core';
import { RxPush } from '@rx-angular/template/push';

@Component({
selector: 'app-hero',
imports: [RxPush],
template: `
<hero-search [term]="searchTerm$ | push" />
<hero-list [heroes]="heroes$ | push" />
`,
})
export class HeroComponent {
// searchTerm$, heroes$ …
}

Select a render strategy per binding with the pipe argument:

<hero-list [heroes]="heroes$ | push: 'immediate'" />

Or pass an object config to also supply a renderCallback or patchZone:

<hero-list [heroes]="heroes$ | push: { strategy: 'normal', patchZone: false }" />

Signature

@Pipe({ name: 'push', pure: false, standalone: true })
class RxPush implements PipeTransform, OnDestroy {
constructor(cdRef: ChangeDetectorRef);

transform<U>(potentialObservable: null, config?: RxStrategyNames | Observable<RxStrategyNames>, renderCallback?: NextObserver<U>): null;
transform<U>(potentialObservable: undefined, config?: RxStrategyNames | Observable<RxStrategyNames>, renderCallback?: NextObserver<U>): undefined;
transform<U>(potentialObservable: ObservableInput<U> | U, config?: RxStrategyNames | Observable<RxStrategyNames>, renderCallback?: NextObserver<U>): U;
transform<U>(potentialObservable: ObservableInput<U>, config?: PushInput<U>): U;

ngOnDestroy(): void;
}

RxPush is not generic: the class-level generic argument was removed; the element type U is inferred per transform call. The pipe uses inject() internally to obtain its RxStrategyProvider and NgZone; its only constructor parameter is cdRef: ChangeDetectorRef.

Object config (PushInput)

The object-config overload accepts:

OptionTypeMeaning
strategyRxStrategyNames \| Observable<RxStrategyNames>The render strategy for this binding.
renderCallbackNextObserver<T>Notified after each render pass completes.
patchZonebooleanWhether the render runs inside NgZone. Inert under zoneless.

See also