rx-angular introduction

Who am I

  • Name: Jia Li
  • Company: ThisDot
  • Zone.js: Code Owner
  • Angular: Collaborator

rx-angular

RxAngular is a reactive library providing extensions for developing high performant applications.

 

Created by Michael Hladky

 

Angular Change Detection

Trigger

1. check

2. render

Trigger Change Detection

  • Auto: Zone.js
  • Manual

Zone.js

<button (click)="clickHandler()"></button>

ngZone.run(() => {
  btn.addEventListener_zonePatchedVersion('click', clickHandler);
});

btn.addEventListener('click' => {
  clickHandler(...);
  onMicrotaskEmpty.emit(null);
});
// ApplicationRef

ngZone.onMicrotaskEmpty.subscribe(() => {
  // run Change Detection from root 
  zone.run(() => tick()); 
});

Zone.js

  • setTimeout/setInterval
  • promise.then
  • XMLHttpRequest
  • addEventListener
  • ....

Pull vs Push based CD

const changePossibleTimings = [XHR, setTimeout, promise.then, 
                               addEventListener, ...];
changePossibleTimings.forEach(t => t.whenCallback(detectChanges();));

Pull base

Push base

change$.subscribe(() => detectChanges());

Other Zone.js issues

  • Unwanted Change detections
  • UnPatched new async APIs
  • native async/await issue (ES2017+) 

Async Pipe?

@Component({
  ...,
  template: `<div>{{data$ | async}}</div>`
})
export class AppComponent {
  data$: Observable<number> = of(1);
}

OnPush?

@Component({
  ...,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  @Input() input;
}

Angular View Tree

Root

Comp A

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

ApplicationRef.tick()

Root

Comp A

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

ChangeDetectionStrategy.Default

Root

Comp A

Default

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

CHECK ALWAYS

ChangeDetectionStrategy.onPush

Root

Comp A

onPush

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

ChangeDetectionStrategy.onPush

AppComponent

OnPush

Component

OnPush

@Component({
  template: `<button (click)="click()"></button>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() input;
  @Output() output = new EventEmitter();
  click() {}
}

<app-push [input]="inputData" (output)="FromPush($event)"></app-push>

When changes happen from the items declared in the template

<app-push></app-push>
@Component({
  template: `<button #btn></button>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() input;
  @Output() output = new EventEmitter();
  @ViewChild('btn') btn; 
  ngOnInit() {
    setTimeout(() => {
      this.input = 'new value';
      this.output.emit('new output');
    });
    this.btn.nativeElement.addEventListener('click', click);
  }

ChangeDetectorRef.markForCheck()

Root

Comp A

onPush

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

markForCheck()

ChangeDetectionRef.detectChanges()

Root

Comp A

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

detectChanges()

ɵmarkDirty()

Root

Comp A

onPush

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

ɵmarkDirty()

animationFrame

detach()/reattach()

Root

Comp A

Comp B

Comp Sub1

Comp Sub2

Comp Sub3

Comp Sub4

detach()

Async Pipe

@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { this.value = v; this.ref.markForCheck(); })
  );

ngrx/component push pipe

@Pipe({name: 'push', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { 
      this.value = v;
      if (hasZone()) {
        this.ref.markForCheck();
      } else {
        this.ref.detectChanges();
      }
    })
  );

rx-angular push pipe

@Pipe({name: 'push', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { 
      this.value = v;
      this.ref.detectChanges();
    })
  );

ChangeDetection Strategies

  • local
  • global
  • native
  • noop
  • detach (experimental)
<div *rxLet="list$; let list; strategy: 'local'"></div>
<hero-list heroes="list$ | push: 'global'"></hero-list>

local strategy

@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { 
      this.value = v;
      this.ref.detectChanges();
    })
  );

global strategy

@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { 
      this.value = v;
      markDirty(comp);
    })
  );

native strategy

@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { 
      this.value = v;
      markForCheck();
    })
  );

noop strategy

@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { 
      this.value = v;
      // do nothing
    })
  );

detach strategy

@Pipe({name: 'async', pure: false})
export class PushPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { 
      this.value = v;
      this.ref.reattach();
      this.ref.detectChanges();
      this.ref.detach();
    })
  );

Coalescing

@Component({
  template: `
    <div (click)="parent()">
      <button (click)="child()">Submit</button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
  parent() {
    console.log('parent');
  }

  child() {
    console.log('child');
  }
}
platformBrowserDynamic()
  .bootstrapModule(AppModule, { ngZoneEventCoalescing: true })

CoalesceWith Operator

AnimationFrame/PromiseTick/setTimeout/...

data$.pipe(
  coalesceWith(animationDurationSelector),
  switchMap((v) => renderTick.pipe(map(() => v))),
  tap(renderMethod)
);

Multiple push pipes with scope

@Component({
selector: 'app-display',
template: `
  {{id$ | push}}
  {{firstName$ | push}}
  {{lastName$ | push}}
`
})
export class DisplayComponent {
  id$ = of(42); 
  firstName$ = of('John'); 
  lastName$ = of('Doe');
}
const scope = component;
data$.pipe(
  coalesceWith(animationDurationSelector, scope),
  switchMap((v) => tick.pipe(map(() => v))),
  tap(renderMethod)
);

viewport-prio directive

<div class="target" viewport-prio></div>

unpatch zone

 <button [unpatch] (click)="click()">click me</button>
<div (scroll#passive,capture,ngManual)="onScroll()"></div>

Reactive context

<ng-container *rxLet="hero$; let hero; 
   rxSuspense: suspenseView; rxError: errorView; rxComplete: completeView">
   {{hero.name}}
<ng-container>
<ng-template #suspenseView>Loading...</ng-template>
<ng-template #errorView>Error!</ng-template>
<ng-template #completeView>Complete.</ng-template>

New features

  • Template View Cache
  • Template rendered callback
  • *rxFor

Performance benchmark

References

  • https://github.com/rx-angular/rx-angular
  • https://indepth.dev/angulars-push-pipe-part-1
  • https://indepth.dev/angulars-push-pipe-part-2
Made with Slides.com