Reactive Angular
How to make reactive rendering in Angular for performance and working in Zone-less world
About me
-
Senior Frontend Developer @
-
Creator of DevNote
Nutti Saelor (Lee)



What is Reactive?

Reactive programming is programming with asynchronous data streams.*

y$ = a$ + b$
y = a + b
Reactive Rendering
Async pipe
@Component({
selector: "async-comp",
template: `
<div>
{{ count$ | async }}
</div>
`
})
export class AsyncPipeComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
Change Detection 101
Trigger Change Detection
- Auto (Zone.js)
- Manual (detectChanges API)
Zone.js
- setTimeout/setInterval
- promise.then
- XMLHttpRequest
- addEventListener
- ...
ApplicationRef.tick()

ApplicationRef.tick()

ChangeDetectionStrategy.Default

ChangeDetectionStrategy.Default

ChangeDetectionStrategy.OnPush

ChangeDetectorRef.markForCheck()

ChangeDetectorRef.markForCheck()

ChangeDetectorRef.detectChanges()

ChangeDetectorRef.detectChanges()

ChangeDetectorRef.detach()

Async pipe
@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
constructor(private _ref: ChangeDetectorRef) {}
// ...
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
}
Zone.js issues
- Unwanted Change Detections
- Unpatched new async API
- Native async/await (ES2017) issue
Zone.js

@ngrx/component
@ngrx/component
- Zoneless Applications
- Push Pipe
- Let Directive
Push Pipe
@Component({
selector: "async-comp",
template: `
<div>
{{ count$ | ngrxPush }}
</div>
`
})
export class AsyncPipeComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
Push Pipe
export function createRender<T>(config: RenderConfig): () => void {
function render() {
if (hasZone(config.ngZone)) {
config.cdRef.markForCheck();
} else {
config.cdRef.detectChanges();
}
}
return render;
}
Let Directive
@Component({
selector: "let-comp",
template: `
<div *ngIf="(remainder$ | async) as remainder">
current remainder : {{ remainder }}
</div>
`
})
export class LetDemoComponent {
remainder$: Observable<number>;
ngOnInit() {
this.remainder$ = interval(1000).pipe(map(num => num % 2));
}
}
Let Directive
@Component({
selector: "with-let",
template: `
<div *ngrxLet="remainder$ as remainder">
current remainder : {{ remainder }}
</div>
`
})
export class WithLetComponent {
remainder$: Observable<number>;
ngOnInit() {
this.remainder$ = interval(1000).pipe(map(num => num % 2));
}
}
@rx-angular/template
@rx-angular/template
- Zoneless Applications
- Push Pipe
- Let Directive
- Optimize for High Performance
Push Pipe
@Component({
selector: "rx-push",
template: `
<div>count : {{ count$ | push }}</div>
`
})
export class RxPushComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
Render Strategies
- local (default)
- global
- noop
- native
- detach (experimental)
Render Strategies

Local Strategy
export function createLocalStrategy<T>(
config: RenderStrategyFactoryConfig
): RenderStrategy {
const component = (config.cdRef as any).context;
const priority = SchedulingPriority.animationFrame;
const tick = priorityTickMap[priority];
const renderMethod = () => {
config.cdRef.detectChanges();
};
const behavior = (o) => // ...
const scheduleCD = <R>(afterCD?: () => R) => // ...
return {
name: 'local',
detectChanges: renderMethod,
rxScheduleCD: behavior,
scheduleCD
};
}
Global Strategy
export function createGlobalStrategy(
config: RenderStrategyFactoryConfig
): RenderStrategy {
const renderMethod = () => markDirty((config.cdRef as any).context);
const cdScheduler = afterScheduleCD(animationFrameTick);
return {
name: 'global',
detectChanges: () => renderMethod(),
rxScheduleCD: (o) => o.pipe(
tap(() => renderMethod()),
switchMap(v => animationFrameTick().pipe(map(() => v)))
),
scheduleCD: <R>(afterCD?: () => R) => {
renderMethod();
return cdScheduler(afterCD);
}
};
}
Noop Strategy
export function createNoopStrategy
(config: RenderStrategyFactoryConfig): RenderStrategy {
return {
name: 'noop',
detectChanges: () => {},
rxScheduleCD: (o) => o.pipe(filter(v => false)),
scheduleCD: () => new AbortController()
};
}
Detach Strategy
export function createDetachStrategy(
config: RenderStrategyFactoryConfig
): RenderStrategy {
const component = (config.cdRef as any).context;
const priority = SchedulingPriority.animationFrame;
const tick = priorityTickMap[priority];
const renderMethod = () => {
config.cdRef.reattach();
config.cdRef.detectChanges();
config.cdRef.detach();
};
const behavior = (o) => // ... ;
const scheduleCD = <R>(afterCD?: () => R) => // ...
return {
name: 'detach',
detectChanges: renderMethod,
rxScheduleCD: behavior,
scheduleCD
};
}
Coalescing
@Component({
selector: 'app-display',
template: `
{{id$ | push}}
{{firstName$ | push}}
{{lastName$ | push}}
`
})
export class DisplayComponent {
id$ = of(42);
firstName$ = of('John');
lastName$ = of('Doe');
}
Coalescing

Push Pipe with Strategy
@Component({
selector: "rx-push",
template: `
<div>count : {{ count$ | push: "noop" }}</div>
`
})
export class RxPushComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
Let Directive
@Component({
selector: "rx-let",
template: `
<div *rxLet="remainder$ as remainder; strategy: 'global'">
current remainder : {{ remainder }}
</div>
`
})
export class RxLetComponent {
remainder$: Observable<number>;
ngOnInit() {
this.remainder$ = interval(1000).pipe(map(num => num % 2));
}
}
Let Directive with Context
<ng-container
*rxLet="
observableNumber$;
let n;
error: error;
complete: complete;
suspense: suspense;
"
>
<app-number [number]="n"></app-number>
</ng-container>
<ng-template #error>ERROR</ng-template>
<ng-template #complete>COMPLETE</ng-template>
<ng-template #suspense>SUSPENSE</ng-template>
New Features
- Viewport Priority
- Unpatch Zone
- Template View Cache
- Template Render Callback
- *rxIf, *rxFor, *rxSwitch
Viewport Priority
Demo
*ngFor vs *rxFor
Native Angular, *ngFor trackBy

*ngFor vs *rxFor
RxAngular, *rxFor trackBy, distinctBy, select

Summary
- Async pipe is boring
- @ngrx/component for Zone-less application
- @rx-angular/template for Zone-less and High Performance application
Thank you!
Reactive Angular
By Lee Lorz
Reactive Angular
- 1,051