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

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