Angular Bad Practices

what to avoid

https://github.com/valentinkononov

http://kononov.space/

 

@ValentinKononov

 

Developer, Speaker

 

 

How do we learn?

  • Find a teacher
  • Book
  • Article
  • Watch YouTube
  • Download boilerplate
  • Just Start Coding
  • Meet issues
  • Stackoverflow

@ValentinKononov

STRICTster

strict settings of angular and compiler

@ValentinKononov

ng new MyAwesomeAngularApp

regular TS settings

regular linter settings

default Angular Compiler settings

any is allowed!

ng new MyAwesomeAngularApp --strict

Angular CLI -> new

@ValentinKononov

{
  "compilerOptions": {
    ...
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
  },
  "angularCompilerOptions": {
    "strictInjectionParameters": true,
    "strictTemplates": true
  }
}

tsConfig.json

-- STRICT

angularCompilerOptions - part of tsconfig

@ValentinKononov

"schematics": {
    "@schematics/angular:application": {
        "strict": true
     }
},
      
...     
"angularCompilerOptions": {
    "strictInjectionParameters": true,
    "strictTemplates": true
}
"fullTemplateTypeCheck": true,
"trace": true,
"preserveWhitespaces": true

-- STRICT

angular.json

other

@ValentinKononov

"no-any": true
"promise-function-async": true
"await-promise": true
"no-async-without-await": true
"no-debugger": true
"no-duplicate-variable": true
"no-var-keyword": true
"max-file-line-count": 140
"no-duplicate-imports": true
...

Or... try esLint

Lint Rules

tslint.json

Links

more info

@ValentinKononov

ANYthing

ANY shall not pass

@ValentinKononov

public ngOnInit(): void {
   const movie = {
       name: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: any): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}
public ngOnInit(): void {
   const movie = {
       title: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: any): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}

'Undefined' in message

ANYthing

@ValentinKononov

ANYthing

@ValentinKononov

interface Movie {
   name: string;
}

public ngOnInit(): void {
   const movie: Movie = {
       name: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: Movie): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}

interfaces and classes

interface Movie {
   title: string;
}

public ngOnInit(): void {
   const movie: Movie = {
       name: 'Game of Thrones',
   };
   this.showToast(movie);
}

private showToast(item: Movie): void {
   this.toaster.show(`Today's movie is: ${item.name}`);
}

TS2339: Property 'name' does not exist on type 'Movie'.

ANYthing

@ValentinKononov

where it is still applicable

  • Generic Decorator
  • Tests
  • Logger
  • No access to props (dirty hacks)

try 'no-any' rules

mitigations

use 'unknown'

export class CustomLineGeometry extends LineGeometry {
    constructor(geometry?: BufferGeometry) {
        super();
        (this as any)._maxInstanceCount = 100;
    }
}

SUBSCRIBE me

@ValentinKononov

ngOnInit() {
    console.log('Initialized');
    this.timerService.initTimer()
      .subscribe(x => {
        this.result = x;
        console.log(x);
      });
}

ngOnDestroy() {
    console.log('Destroyed');
}

ISSUE: Memory Leak

SUBSCRIBE me

@ValentinKononov

  • memory leaks
  • doesn't work with OnPush

Issues

@ValentinKononov

subscription: Subscription;

ngOnInit() {
    console.log('Initialized');
    this.subscription = this.timerService.initTimer()
      .subscribe(x => {
        this.result = x;
        console.log(x);
      });
}

ngOnDestroy() {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
    console.log('Destroyed');
}

Manual Unsubscribe

@ValentinKononov

  • no leaks
  • a lot of manual work -> human mistakes
  • doesn't work with OnPush

Issues

@ValentinKononov

alive = true;

ngOnInit() {
    console.log('Initialized');
    this.timerService.initTimer()
      .pipe(takeWhile(() => {
        console.log('check'); return this.alive; }))
      .subscribe(x => {
        this.result = x;
        console.log(x);
      });
}

ngOnDestroy() {
    this.alive = false;
    console.log('Destroyed');
}

takeWhile

@ValentinKononov

  • no leaks
  • BUT still, have 1 extra subscription tick
  • easy to use, less code
  • doesn't work with OnPush

Issues

@ValentinKononov

unsubscribe: Subject<void> = new Subject();

ngOnInit() {
    console.log('Initialized');
    this.timerService.initTimer()
      .pipe(takeUntil(this.unsubscribe))
      .subscribe(x => {
        this.result = x;
        console.log(x);
      });
}

ngOnDestroy() {
    this.unsubscribe.next();
    this.unsubscribe.complete();
    console.log('Destroyed');
}

takeUntil should be the LAST in sequence

takeUntil

@ValentinKononov

  • no leaks
  • a lot of boilerplate code -> human mistakes
  • doesn't work with OnPush

Issues

@ValentinKononov

import {untilDestroyed} from "ngx-take-until-destroy";

ngOnInit() {
    console.log('Initialized');
    this.timerService.initTimer()
      .pipe(untilDestroyed(this))
      .subscribe(x => {
        this.result = x;
        console.log(x);
      });
}

ngOnDestroy() {
    console.log('Destroyed');
}

untilDestroyed

@ValentinKononov

  • no leaks
  • less code
  • doesn't work with OnPush
  • thrid party dependency

Issues

@ValentinKononov

Async Pipe

@ValentinKononov

@Component({ 
 template: `
	<span>
		{{ this.service.getData() | async }}
	</span>
`,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SubscriptionComponent {
	constructor(private service: Service) {}  
} 

NOTE: change detection will trigger each time when Observable emits value

Async Pipe

@ValentinKononov

  • no leaks
  • WORK with OnPush
  • a minimal amount of code
  • cannot use in TS component code as is
  • cannot use in several places in template

Issues

@ValentinKononov

@Component({ 
 template: `<span>{{ this.result$ | async }}</span>`,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SubscriptionComponent 
    implements OnInit, OnDestroy {
  
    result$: Observable<number>;

    ngOnInit() {
        console.log('Initialized');
        this.result$ = this.timerService.initTimer()
          .pipe(tap(x => { console.log(x); }));
    }

    ngOnDestroy() {
        console.log('Destroyed');
    }
} 

Async Pipe

@ValentinKononov

usage in TS and template

  • no leaks
  • WORK with OnPush
  • a minimal amount of code
  • cannot use in several places in template

Issues

@ValentinKononov

NOTE: actually can use in several places, but there will be N triggers of observable

<ng-container *ngIf="{
	id: id$ | async,
	count: count$ | async
} as data">
	Id: {{ data.id }}
	Count: {{ data.count }}
</ng-container>

Async Pipe - template work

@ValentinKononov

id$: Observable<number>;
count$: Observable<number>;

*ngIf scope - several obsevables

<div>
	Id: {{ id$ | async }}
	Count: {{ count$ | async }}
</div>
id$: Observable<number>;
count$: Observable<number>;

ngOnInit() {
	console.log('Initialized');
	this.result$ = this.timerService
      	.initTimerForObject()
      	.pipe(tap(x => {
        	console.log(`Timer: ${x}`);
      	}));
    this.id$ = this.result$.pipe(map(x => x.id));
    this.count$ = this.result$.pipe(map(x => x.count));
}

Async Pipe - template work

several obsevables

  • no leaks
  • WORKs with OnPush
  • a minimal amount of code
  • CAN use in TS code as is
  • CAN use in several places in the template
  • Amount if requests INCREASED

Issues

@ValentinKononov

SHAREing

@ValentinKononov

<div>
	Subscription result: 
	{{ id$ | async }} | {{ count$ | async }}
</div>

@ValentinKononov

Share Pipe

id$: Observable<number>;
count$: Observable<number>;

ngOnInit() {
	this.result$ = this.timerService
      	.initTimerForObject()
      	.pipe(share());
  
    this.id$ = this.result$.pipe(map(x => x.id));
    this.count$ = this.result$.pipe(map(x => x.count));
}
  • no leaks
  • WORKs with OnPush
  • a minimal amount of code
  • CAN use in TS code as is
  • CAN use in several places in the template
  • Observable is reused

Issues

@ValentinKononov

share()

publish(), refCount()

observer1
observer2
Subject
Source

Dive Deeper into RxJs

@ValentinKononov

publishReplay(), refCount()

shareReplay({refCount: true})

All new Subscribers will get N Buffered values

Multicast Pipes

shareReplay()

@ValentinKononov

Out of the woods

@ValentinKononov

id$: Observable<number>;
count$: Observable<number>;

ngOnInit() {
	this.result$ = this.timerService
      	.initTimerForObject()
      	.pipe(
      		share(),
		);
  
    this.id$ = this.result$.pipe(map(x => x.id));
    this.count$ = this.result$.pipe(map(x => x.count));
}

Errors in observables

<div>{{ id$ | async }} | {{ count$ | async }}</div>

@ValentinKononov

<div>{{ id$ | async }} | {{ count$ | async }}</div>

catchError

@ValentinKononov

id$: Observable<number>;
count$: Observable<number>;

ngOnInit() {
	this.result$ = this.timerService
      	.initTimerForObject()
      	.pipe(
      		share(),
      		catchError((error: any) => {
			console.log(error);
			return of({ id: 0, count: 0 });
        }),
    );
  
    this.id$ = this.result$.pipe(map(x => x.id));
    this.count$ = this.result$.pipe(map(x => x.count));
}

rxjs-no-ignored-subscription

rxjs-no-ignored-subscribe

rxjs-no-ignored-observable

rxjs-no-ignored-error

rxjs-no-nested-subscribe

rxjs-no-unsafe-takeuntil

rxjs-prefer-async-pipe

 

 

https://www.nmpjs.com/package/rxjs-tslint-rules

Lint it

@ValentinKononov

PIPE it

@ValentinKononov

<button type="button" (click)="sortCollection()">
    Sort Collection</button>
<div *ngFor="let item of items; trackBy: trackByFn">
	{{ getLabel(item) }}
</div>

PIPE it

@ValentinKononov

function in the template

getLabel(item:  ItemData): string {
    console.log(`Item ${item.id} label call`);
    return `Item ${item.id}`;
}
@Pipe({ name: 'label'})
export class LabelPipe implements PipeTransform {
  transform(value: Item, args?: any): string {
    console.log(`Item ${value} label call`);
    return `Item ${value.id}`;
  }
}

PIPE it

@ValentinKononov

<button type="button" (click)="sortCollection()">
    Sort Collection</button>
<div *ngFor="let item of items; trackBy: trackByFn">
	{{ item | label }}
</div>

function as angular pipe

items: ItemData[] = [{
      parentId: 1,
      name: 'Game Of Thrones',
    },
    {
      parentId: 2,
      name: 'The Lord of the Rings',
    },
    {
      parentId: 3,
      name: 'Big Bang Theory - Season 1',
    },
    {
      parentId: 3,
      name: 'Big Bang Theory - Season 2',
    }];

PIPE it

@ValentinKononov

change detection

@ValentinKononov

Conclusion

  • Set stricter settings for your TypeScript and Avoid ANY
  • Use Async Pipes for subscription
  • Use share() for multiple usages
  • Use Pipes instead functions in templates
  • Enjoy coding!

@ValentinKononov

Bad Practices

what to avoid in Angular

Thanks! See Ya

@ValentinKononov

Angular Bad Practices Heidelberg

By Valentin Kononov

Angular Bad Practices Heidelberg

improved Bad Practices talk for ngHeidelberg meetup, July 2020

  • 131

More from Valentin Kononov