Angular has undoubtedly grown at a great pace recently. It has become one of the most used and popular frameworks for cross-platform and front-end web applications. Besides providing tons of out-of-the-box features like dependency injection framework, routing system, forms handling, etc, Angular allows developers to use both RxJs and TypeScript. This is because it is already a vital part of the Angular ecosystem. This wide range of features is what makes Angular a great candidate for larger enterprises.
Although Angular is an exceptionally powerful framework, it is quite hard to master the framework. Despite being accompanied by a wide toolkit, in case you are a beginner and it is your first ever JavaScript framework, you can face hurdles.
To reduce the complexity, this guide features the practices that are used in applications related to Angular, RxJs, Typescript, and @ngrx/store. Below is a clean code checklist to write clean and production-ready Angular code.
Make sure you use pipeable operators while using RxJs operators.
Here’s why: Pipeable operators are generally Tree-shakeable(i.e unused modules will not be included in the bundle during the build process). Furthermore, this makes it quite easy to spot unused operators present in the files.
Before
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
myObservable
.map(value => value.item)
.take(1);
After
import { map, take } from 'rxjs/operators';
myObservable
.pipe(
map(value => value.item),
take(1)
);
Ensure using appropriate operators for respective situations while you use flattening operators with your observables.
Here’s why: Use a single operator whenever possible in place of chaining several other operators together for obtaining the same effect. This can lead to fewer codes required to ship to the user. In case any inappropriate operator is used, it can cause unwanted behaviour. This is because distinct operators manage observables in distinct ways.
While you use ngFor to loop over a set in templates, use it by a trackBy function. This will yield a unique identifier for every item.
Here’s why: Angular re-renders the entire DOM tree in case any array gets modified. However, using trackBy helps Angular to identify the changed element. Hence, it will make DOM modifications related to that specific element.
Before
<li *ngFor="let item of items;">{{ item }}</li>
After
// In your template
<li *ngFor="let item of items; trackBy: trackByFun">{{ item }}</li>
// In your component
trackByFun(index, item) {
return item.id; // unique id corresponding to the item
}
Make use of const when the value of the declared variable will not be reassigned.
Here’s why:The intention of the declarations becomes clear when let
and const
are used appropriately. By compile-time error, when you reassign a value to a constant accidentally, it becomes a problem. Here, let and const helps to identify issues that improve the code readability.
Before
let MinutesInHour = 60;
let babyNapMinutes = 0;
if (babySlept) {
babyNapMinutes = babyNapMinutes + (sleepHours * MinutesInHour);
}
After
// the value of MinutesInHour will not change hence we can make it a const
const MinutesInHour = 60;
let babyNapMinutes = 0;
if (babySlept) {
babyNapMinutes = babyNapMinutes + (sleepHours * MinutesInHour);
}
Ensure unsubscribing when you subscribe to observables properly. You can use operators such as take
or takeUntil
for the same. take
comes handy when you need only the first value emitted. takeUntil
is used to listen to an observable until another observable emits a value.
Here’s why: If you fail to unsubscribe from observables, you can experience useless memory leaks. This is because the observable stream stays open after destroying a component.
A better option will be to create a lint rule to detect the unsubscribed observables.
Before
myObservable$
.pipe(
map(value => value.item)
)
.subscribe(item => this.displayText = item);
After
private subDestroyed$ = new Subject();
public ngOnInit (): void {
myObservable$
.pipe(
map(value => value.item),
take(1),
// We want to listen to myObservable$ until the component is destroyed,
takeUntil(this.subDestroyed$)
)
.subscribe(item => this.displayText = item);
}
public ngOnDestroy (): void {
this.subDestroyed$.next();
this.subDestroyed$.complete();
}
Pay attention to the takeUntil
along with take
here. This helps to restrain the potential memory leaks. Leaks usually take place when the subscription does not receive a value before the component gets destroyed. If you don’t use takeUntil
, the subscription will still hang unless it received the first value. However, as the component is already destroyed, it will certainly not receive a value. Hence, memory leaks may take place.
To perform certain actions, you may need values from several observables. In such a case, don’t subscribe to one observable in the subscribe block of the second observable. Rather, utilize relevant chaining operators. Chaining operators generally run on observables from the operator ahead of them. Few chaining operators include combineLatest, withLatestFrom, etc.
Here’s why:In case the observables are cold, it subscribes to first observable. Wait for it while it processes and finishes off. Next, start with the second observable work. In case these were network requests, it would display as a waterfall.
Before
aObservable$
.pipe(
take(1)
)
.subscribe(aValue => {
bObservable$.pipe(
take(1)
)
.subscribe(bValue => {
console.log(`Combined values are: ${aValue} & ${bValue}`);
});
});
After
aObservable$.pipe(
withLatestFrom(bObservable$),
first()
)
.subscribe(([aValue, bValue]) => {
console.log(`Combined values are: ${aValue} & ${bValue}`);
});
Draw out the reusable pieces in a component and make it new. Make sure you create the component as dumb as possible. This will help it to work in several other frameworks. Here, dumb means zero logic. When you make the component dumb, it does not comprise any unique logic in it. Hence, it operates entirely on the basis of inputs and outputs provided to it.
Here’s why: Reusable components lowers the rate of duplication of codes. Hence, it becomes easy to manage and make modifications to it.
Dumb components are uncomplicated and have fewer bugs. Dumb components tend to think more about the public component API thereby digging out varied concerns.
Long methods usually demonstrate that they are undergoing too many things. Try using the Single Responsibility Principle. While the method might undergo one operation, there are several other operations that can take place. You can draw out those methods separately so that they perform one operation at a time.
Here’s why: Long methods are complicated and hard to maintain and read. Furthermore, they are likely to attract bugs and create difficulties in refactoring. Refactoring is a key thing in every application and cannot be compromised.
Several ESLint Rules exist for the detection of bugs and to identify code smell issues. You can use these in your project to prevent bugs and identify code smells/issues.
Make sure you don’t subscribe to observables from components. Rather, subscribing to observables from templates is better.
Here’s why:In case, you forget to unsubscribe from a subscription in the component, you may experience a memory leak. async
pipes automatically unsubscribe themselves by removing the need to manage subscriptions manually. This makes it way simpler. It lowers the possibility of forgetting to unsubscribe any subscription in the component.
This risk can further be reduced by making use of a lint rule for detecting unsubscribed observables.
This further prevents the components to introduce bugs where the data undergoes modification outside the subscription.
Before
// In template
Hello {{ displayText }}
// In component
myObservable$
.pipe(
map(value => value.item),
takeUntil(this.subDestroyed$)
)
.subscribe(item => this.displayText = item);
After
// In template
Hello {{ displayText$ | async }}
// In component
this.displayText$ = myObservable
.pipe(
nbsp; map(value => value.item)
);
Try to lazy load the Angular application modules whenever possible. Lazy loading refers to a situation when you load something only when it is used.
Here’s why:Lazy loads reduce the overall size of the application that is to be loaded. It enhances the boot time of the application by not loading unused modules.
Before
// In app.routing.ts
{ path: 'not-lazy-loaded-module', component: NotaLazyLoadedComponent }
After
// In app.routing.ts
{
path: 'lazy-load',
loadChildren: 'lazy-load.module#LazyLoadedModule'
}
// lazy-load.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { LazyLoadedComponent } from './lazy-load.component';
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: LazyLoadedComponent
}
])
],
declarations: [
LazyLoadedComponent
]
})
export class LazyModule {}
Here’s why:Type of the Constants or variables declared in TypeScript without a type will be deduced using the value assigned to it. Using any or missing types will cause the unwanted issues. These issues can be avoided by good typing practices.
Proper typing will also make code refactoring easy, reducing the changes of error
Before
public ngOnInit (): void {
let personObj: PersonObject = {
name: 'Johny',
age: 30
}
this.displayDetails(personObj);
}
public displayDetails(myObject: any): void {
console.log(`Full Name: ${myObject.Name}`);
myObject.age = 'a';
console.log(`Age: ${myObject.age}`);
}
// Output
Full Name: undefined
Age: a
Accessing undefined property Name
in this case does not throw error. Similarly, assigning string to age
does not throw error as well.
If we renamed the property name
to fullName
with proper typing of PersonObject
, accessing old property name
will throw compilation error. Declaring age as number will throw compilation error if some other types are assigned to it.
After
type PersonObject = {
fullName: string,
age: number
}
public ngOnInit (): void {
let personObj: PersonObject = {
// Compilation error
Type '{ fullName: string; age: number; }' is not assignable to type 'PersonObjectType'.
Object literal may only specify known properties, and 'name' does not exist in type 'PersonObjectType'.
name: 'Johny',
// Compilation error
// This will give a compile error saying:
Type '"Thirty"' is not assignable to type 'number'.
const age:number
age: 'Thirty'
}
this.displayDetails(personObj);
}
public displayDetails(myObject: PersonObject): void {
// Compilation error
Property 'name' does not exist on type 'PersonObjectType'.
console.log(`Full Name: ${myObject.name}`);
console.log(`Age: ${myObject.age}`);
}
Refrain from having any logic apart from display logic in your component whenever possible.
Here’s why:Components are built for presentational purposes. It handles the role of the view. Each business logic must be drawn out into its own services wherever relevant, thus parting business logic from view logic in addition to making the service unit-testable and reusable.
ESLint statically analyses the code to quickly find problems. ESLint is built into most text editors and can be run as part of your continuous integration pipeline. ESLint can be be configured in .eslintrc.js file at the root of project.
ESLint has plethora of rules ranging from common errors (eg. getter-return) to best practices (eg. max-classes-per-file)
Even if its a small === check in your template, move them to its own component
Here’s why:Having logics in templates makes it more prone to bugs and makes it difficult to unit test it.
Before
//In template
Subject is lesser than or equal to 4
//In component
public ngOnInit (): void {
this.subject = 3;
}
After
//In template
//In component
public greaterThan(subj: Subject, num: number) {
return sub < num;
}
While making API calls, few responses remain unaltered. This is when you can add a caching mechanism thereby storing the value from the API. Later, when you make another request to the same API, check for any existing value in the cache. If you find any, utilize it. Or, cache the result by making the API call. In case the values alter, but not regularly, build a cache time. This cache time will help you to keep a check on when it was cached last. Later, you can even decide if you want to make a call to the API or not.
Here’s why: Adding a caching mechanism avoids unwanted API calls. With this, the pace of the application elevates as well. This is because you can make API calls whenever required, and thus it prevents duplication as well. Caching mechanism also means that no same information is downloaded again and again.
Maintaining state and side effects of an application is so inevitable. @ngrx is most widely used library. Make sure you use @ngrx/store
to nurture the state of your application and @ngrx/effects
as the side effect model.
Here’s why: @ngrx/store
detaches all the state-linked logic in one place thereby making it consistent over the app. Angular’s change detection in combination with #ngrx/store
results in performant application
Angular is an exceptional framework that offers excellent functionality and features. And if you are a newbie in this field, it can definitely be overwhelming with such a variety of features. In fact, building applications is a consistent process. Hence, there is always room to enhance things.
With this, we come to the end of this discussion. By following the above-listed guidelines, we hope we could help you with some of the concepts. When you implement these patterns in your work, it becomes extremely rewarding. Moreover, you provide an excellent user experience with a less buggy and performant application. Our smart and experienced Angular developers are well-acquainted with these Angular best practices and will make your dream app a successful reality.