How to make reactive rendering in Angular for performance and working in Zone-less world
Senior Frontend Developer @
Creator of DevNote
@Component({
selector: "async-comp",
template: `
<div>
{{ count$ | async }}
</div>
`
})
export class AsyncPipeComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
@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();
}
}
}
@Component({
selector: "async-comp",
template: `
<div>
{{ count$ | ngrxPush }}
</div>
`
})
export class AsyncPipeComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
export function createRender<T>(config: RenderConfig): () => void {
function render() {
if (hasZone(config.ngZone)) {
config.cdRef.markForCheck();
} else {
config.cdRef.detectChanges();
}
}
return render;
}
@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));
}
}
@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));
}
}
@Component({
selector: "rx-push",
template: `
<div>count : {{ count$ | push }}</div>
`
})
export class RxPushComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
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
};
}
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);
}
};
}
export function createNoopStrategy
(config: RenderStrategyFactoryConfig): RenderStrategy {
return {
name: 'noop',
detectChanges: () => {},
rxScheduleCD: (o) => o.pipe(filter(v => false)),
scheduleCD: () => new AbortController()
};
}
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
};
}
@Component({
selector: 'app-display',
template: `
{{id$ | push}}
{{firstName$ | push}}
{{lastName$ | push}}
`
})
export class DisplayComponent {
id$ = of(42);
firstName$ = of('John');
lastName$ = of('Doe');
}
@Component({
selector: "rx-push",
template: `
<div>count : {{ count$ | push: "noop" }}</div>
`
})
export class RxPushComponent {
count$: Observable<number>;
ngOnInit() {
this.count$ = interval(1000);
}
}
@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));
}
}
<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>
Demo
Native Angular, *ngFor trackBy
RxAngular, *rxFor trackBy, distinctBy, select