📜 ⬆️ ⬇️

22 Angular Development Board. Part 1

The author of the article, the first part of the translation of which we publish, says that he has been working on a large-scale Angular application in Trade Me for about two years now. Over the past few years, the application development team has been constantly engaged in improving the project, both in terms of code quality and performance.


This series of materials focuses on development approaches used by the Trade Me team, which are expressed in more than two dozen recommendations regarding technologies such as Angular, TypeScript, RxJS, and @ ngrx / store. In addition, some attention will be paid here to universal programming techniques that are aimed at making the application code cleaner and more accurate.

1. About trackBy


Using ngFor to traverse arrays in templates, use this construct with the trackBy function, which returns a unique identifier for each element.

â–Ť Explanations


When the array is changed, Angular re-renders the entire DOM tree. But if you use trackBy , the system will know which element has changed and will make changes to the DOM that relate only to that particular element. Details about this can be found here .
')

â–ŤTo


 <li *ngFor="let item of items;">{{ item }}</li> 

â–ŤAfter


 //   <li *ngFor="let item of items; trackBy: trackByFn">{{ item }}</li> //   trackByFn(index, item) {     return item.id; //  id,   } 

2. Keywords const and let


If you are going to declare a variable whose value is not planned to be changed, use the keyword const .

â–Ť Explanations


The appropriate use of the let and const keywords clarifies the intent regarding the use of entities declared with their help. In addition, this approach makes it easier to recognize problems caused by accidentally overwriting constant values. In such a situation, a compilation error is generated. In addition, it improves the readability of the code.

â–ŤTo


 let car = 'ludicrous car'; let myCar = `My ${car}`; let yourCar = `Your ${car}; if (iHaveMoreThanOneCar) {  myCar = `${myCar}s`; } if (youHaveMoreThanOneCar) {  yourCar = `${youCar}s`; } 

â–ŤAfter


 //  car  ,     car  const car = 'ludicrous car'; let myCar = `My ${car}`; let yourCar = `Your ${car}; if (iHaveMoreThanOneCar) {  myCar = `${myCar}s`; } if (youHaveMoreThanOneCar) {  yourCar = `${youCar}s`; } 

3. Conveyable operators


When working with RxJS, use pipelined operators.

â–Ť Explanations


Conveyable operators support the tree-shake algorithm, that is, when they are imported, only the code that you plan to execute will be included in the project. It also simplifies the identification of unused statements in files.

Please note that this recommendation is relevant for Angular version 5.5 and higher.

â–ŤTo


 import 'rxjs/add/operator/map'; import 'rxjs/add/operator/take'; iAmAnObservable   .map(value => value.item)   .take(1); 

â–ŤAfter


 import { map, take } from 'rxjs/operators'; iAmAnObservable   .pipe(      map(value => value.item),      take(1)    ); 

4. API patch isolation


Not all APIs are completely stable and free from errors. Therefore, it is sometimes necessary to enter into the code some kind of logic aimed at fixing API problems. Instead of placing this logic in the components where the updated API is used, it would be better to isolate it somewhere, for example, in the service, and turn from the component to the corresponding service rather than the problematic API.

â–Ť Explanations


The proposed approach allows you to keep fixes “closer” to the API, that is, as close to the code from which network requests are executed as possible. As a result, the amount of application code interacting with problematic APIs is reduced. In addition, it turns out that all corrections are in one place, as a result it will be easier to work with them. If you have to correct errors in the API, then it is much easier to do this in a single file than to scatter these corrections throughout the application. This makes it easier not only to create fixes, but also to find the corresponding code in the project, and its support.

In addition, you can create your own tags, like API_FIX (which resembles the TODO tag), and tag them with fixes. This makes finding such fixes easier.

5. Subscription in the template


Avoid subscribing to observable objects from components. Instead, subscribe to them in templates.

â–Ť Explanations


Asynchronous pipelined operators automatically unsubscribe themselves, which simplifies the code, eliminating the need for manual subscription management. It also reduces the risk that the developer will forget to unsubscribe from the component in the component, which can lead to memory leaks. The possibility of memory leaks can also be reduced by using linter rules aimed at identifying observable objects that have not been unsubscribed.

In addition, the use of this approach leads to the fact that the components cease to be state components, which can lead to errors when the data changes outside the subscription.

â–ŤTo


 //  <p>{{ textToDisplay }}</p> //  iAmAnObservable   .pipe(      map(value => value.item),      takeUntil(this._destroyed$)    )   .subscribe(item => this.textToDisplay = item); 

â–ŤAfter


 //  <p>{{ textToDisplay$ | async }}</p> //  this.textToDisplay$ = iAmAnObservable   .pipe(      map(value => value.item)    ); 

6. Remove Subscriptions


When subscribing to observable objects, always ensure that subscriptions to them are properly deleted using operators like take , takeUntil and so on.

â–Ť Explanations


If you do not unsubscribe from the observed object, this will lead to memory leaks, since the flow of the observed object will remain open, which is possible even after the component is destroyed or after the user moves to another page of the application.

It would be even better to create a linter rule to detect monitored objects with a valid subscription to them.

â–ŤTo


 iAmAnObservable   .pipe(      map(value => value.item)        )   .subscribe(item => this.textToDisplay = item); 

â–ŤAfter


Use the takeUntil operator if you want to monitor changes in an object until another observed object generates a certain value:

 private destroyed$ = new Subject(); public ngOnInit (): void {   iAmAnObservable   .pipe(      map(value => value.item)     //    iAmAnObservable         takeUntil(this._destroyed$)    )   .subscribe(item => this.textToDisplay = item); } public ngOnDestroy (): void {   this._destroyed$.next(); } 

Using something like this is a pattern used to control the removal of subscriptions to many of the observed objects in a component.

Use take if you only need the first value returned by the observed object:

 iAmAnObservable   .pipe(      map(value => value.item),      take(1),      takeUntil(this._destroyed$)   )   .subscribe(item => this.textToDisplay = item); 

Please note that here we use takeUntil with take . This is done in order to avoid memory leaks caused by the fact that the subscription did not result in a value before the component was destroyed. If the takeUntil function were not used takeUntil , the subscription would exist before the first value was received, but since the component would have already been destroyed, this value would never have been received, which would lead to a memory leak.

7. Use suitable operators


Using smoothing operators with observable objects, use those that correspond to the features of the problem being solved.


Details about this can be found here .

â–Ť Explanations


The use of one operator, if possible, instead of achieving the same effect by chaining several operators, results in a reduction in the amount of application code that needs to be sent to the user. Using an unsuccessfully selected operator can lead to incorrect program behavior, since different operators treat observable objects differently.

8. Lazy loading


Then, when possible, try to organize a lazy loading modules Angular-application. This technique boils down to the fact that something is loaded only if it is used. For example - the component is loaded only when it needs to be displayed on the screen.

â–Ť Explanations


Lazy loading reduces the size of the application materials that the user has to download. This can improve the download speed of the application due to the fact that unused modules are not transferred from the server to clients.

â–ŤTo


 // app.routing.ts { path: 'not-lazy-loaded', component: NotLazyLoadedComponent } 

â–ŤAfter


 // app.routing.ts { path: 'lazy-load', loadChildren: 'lazy-load.module#LazyLoadModule' } // lazy-load.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { LazyLoadComponent }   from './lazy-load.component'; @NgModule({ imports: [   CommonModule,   RouterModule.forChild([        {            path: '',            component: LazyLoadComponent        }   ]) ], declarations: [   LazyLoadComponent ] }) export class LazyModule {} 

9. About subscriptions within other subscriptions


Sometimes, to perform an action, you may need data from several monitored objects. In this situation, avoid creating subscriptions for such objects inside the subscribe blocks of other monitored objects. Instead, use the appropriate operators to chain the commands together. Among such operators can be noted withLatestFrom and combineLatest . Consider the examples, and then comment on them.

â–ŤTo


 firstObservable$.pipe(  take(1) ) .subscribe(firstValue => {   secondObservable$.pipe(       take(1)   )   .subscribe(secondValue => {       console.log(`Combined values are: ${firstValue} & ${secondValue}`);   }); }); 

â–ŤAfter


 firstObservable$.pipe(   withLatestFrom(secondObservable$),   first() ) .subscribe(([firstValue, secondValue]) => {   console.log(`Combined values are: ${firstValue} & ${secondValue}`); }); 

â–Ť Explanations


If we talk about readability, the complexity of the code, or signs of bad code, when the program does not fully utilize the RxJS capabilities, it means that the developer is not familiar enough with the RxJS API. If we touch on the topic of performance, then it turns out that if the observed object needs some time for initialization, it will subscribe to firstObservable , then the system will wait for the operation to complete, and only after that will work with the second observed object. If these objects are network requests, then it will look like synchronous execution of requests.

10. About typing


Always try to declare variables or constants with a type other than any .

â–Ť Explanations


When a variable or a constant is declared in TypeScript, the type will be derived based on the value assigned to it. This can lead to problems. Consider a classic example of system behavior in a similar situation:

 const x = 1; const y = 'a'; const z = x + y; console.log(`Value of z is: ${z}` //  Value of z is 1a 

It is assumed that y is a number here, but our program does not know about it, so it displays something that looks wrong, but does not give any error messages. Similar problems can be avoided by assigning appropriate types to variables and constants.

Rewrite the above example:

 const x: number = 1; const y: number = 'a'; const z: number = x + y; //    : Type '"a"' is not assignable to type 'number'. const y:number 

This helps to avoid data type errors.

Another advantage of a systematic approach to typing is that it simplifies refactoring and reduces the likelihood of errors during this process.

Consider an example:

 public ngOnInit (): void {   let myFlashObject = {       name: 'My cool name',       age: 'My cool age',       loc: 'My cool location'   }   this.processObject(myFlashObject); } public processObject(myObject: any): void {   console.log(`Name: ${myObject.name}`);   console.log(`Age: ${myObject.age}`);   console.log(`Location: ${myObject.loc}`); } //  Name: My cool name Age: My cool age Location: My cool location 

Suppose that we wanted to change, in the myFlashObject object, the name of the property loc on location and made an error during code editing:

 public ngOnInit (): void {   let myFlashObject = {       name: 'My cool name',       age: 'My cool age',       location: 'My cool location'   }   this.processObject(myFlashObject); } public processObject(myObject: any): void {   console.log(`Name: ${myObject.name}`);   console.log(`Age: ${myObject.age}`);   console.log(`Location: ${myObject.loc}`); } //  Name: My cool name Age: My cool age Location: undefined 

If typing is not used when creating myFlashObject , then in our case the system assumes that the value of the loc property of myFlashObject is undefined . She does not think that loc can be an invalid property name.

If typing is used when describing the myFlashObject object, then in a similar situation we will see, when compiling the code, a wonderful error message:

 type FlashObject = {   name: string,   age: string,   location: string } public ngOnInit (): void {   let myFlashObject: FlashObject = {       name: 'My cool name',       age: 'My cool age',       //         Type '{ name: string; age: string; loc: string; }' is not assignable to type 'FlashObjectType'.       Object literal may only specify known properties, and 'loc' does not exist in type 'FlashObjectType'.       loc: 'My cool location'   }   this.processObject(myFlashObject); } public processObject(myObject: FlashObject): void {   console.log(`Name: ${myObject.name}`);   console.log(`Age: ${myObject.age}`)   //     Property 'loc' does not exist on type 'FlashObjectType'.   console.log(`Location: ${myObject.loc}`); } 

If you are starting work on a new project, it will be useful to set the strict:true option in the tsconfig.json file to enable strict type checking.

11. About using linter


Tslint has various standard rules like no-any , no-magic-numbers , no-console . The linter can be configured by editing the tslint.json file in order to organize the checking of code according to certain rules.

â–Ť Explanations


Using linter to verify the code means that if there is something in the code that is prohibited by the rules, you will receive an error message. This contributes to the uniformity of the project code, improves its readability. Here you can read other tslint rules.

It should be noted that some rules also include means of correcting what is considered inadmissible according to them. If necessary, you can create your own rules. If you're interested, take a look at this material, which deals with creating your own rules for the linter using TSQuery .

â–ŤTo


 public ngOnInit (): void {   console.log('I am a naughty console log message');   console.warn('I am a naughty console warning message');   console.error('I am a naughty console error message'); } // .    ,    : I am a naughty console message I am a naughty console warning message I am a naughty console error message 

â–ŤAfter


 // tslint.json {   "rules": {       .......       "no-console": [            true,            "log", //  console.log             "warn" //  console.warn        ]  } } // ..component.ts public ngOnInit (): void {   console.log('I am a naughty console log message');   console.warn('I am a naughty console warning message');   console.error('I am a naughty console error message'); } // .      console.log and console.warn        console.error,         Calls to 'console.log' are not allowed. Calls to 'console.warn' are not allowed. 

Results


Today we reviewed 11 recommendations that we hope will be useful for Angular-developers. Next time, wait for 11 more tips.

Dear readers! Do you use the Angular framework for developing web projects?

Source: https://habr.com/ru/post/425661/


All Articles