Technology

Must-Know Clean Code Principles in Angular

Kirthika Selvaraj

May 4, 2023
Table of contents

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.

1. Pipeable operators

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)
);

2. Utilize Proper Operators

Ensure using appropriate operators for respective situations while you use flattening operators with your observables.

  • SwitchMap: You can use this when you wish to avoid the prior emissions when you find a new emission.
  • MergeMap: You can use this when you wish to handle each of the emissions simultaneously.
  • ConcatMap: This can be used when you want to manage the emissions one by one as they are emitted.
  • ExhaustMap: This can be used when you want to dismiss every new emission while processing a foregoing emission.

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.

3. trackBy

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;"&gt{{ item }}</li>

After

// In your template
<li *ngFor="let item of items; trackBy: trackByFun"&gt{{ item }}</li>
// In your component
trackByFun(index, item) {
return item.id; // unique id corresponding to the item
}

4. const VS let

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);
}

5. Clear out the subscriptions

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.

6. Restrain owning subscriptions inside subscriptions

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}`);
});

7. Tiny reusable components

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.

8. Refrain long methods

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.

9. Subscribe in templates

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)
);

10. Lazy Load

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 {}

11. Use appropriate type and not any

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}`);
}

12. No business logic in Components

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.

13. Use ESLint

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)

14. Keep template free from logics

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;
}

15. Avoid duplicate API calls

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.

16. State Management

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

Conclusion

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.

Latest writings