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​