Best practices for clean Angular project

Introduction :

Hello Dear friends, this is my first blog in the 2021 series and I will start by writing about Angular technology best practices,

I want to share with you what I have learned through other blogs and community recommendation about the best tips and tricks to make a clean code with Angular and make your application performance and scalable because I believe clean code is the door to a good software project

We will discuss project structure, typescript tips, angular features tricks for performance and optimization

Happy reading :)

Let’ start :

Project Structure : 📓 📕

Before starting a new Angular project, you should always care of your folder structure and follow the community recommendation that should be set up

|-- app
|-- modules
|-- home
|-- [+] components
|-- [+] pages
|-- home-routing.module.ts
|-- home.module.ts
|-- core
|-- [+] authentication
|-- [+] footer
|-- [+] guards
|-- [+] http
|-- [+] interceptors
|-- [+] mocks
|-- [+] services
|-- [+] header
|-- core.module.ts
|-- ensureModuleLoadedOnceGuard.ts
|-- logger.service.ts
|
|-- shared
|-- [+] components
|-- [+] directives
|-- [+] pipes
|-- [+] models
|
|-- [+] configs
|-- assets
|-- scss
|-- [+] partials
|-- _base.scss
|-- styles.scss

Shared Module:

|-- shared
|-- [+] components
|-- [+] directives
|-- [+] pipes
|-- [+] models

This Shared Module should contain the common models, pipes and directives, and common components for the whole project.

  • Components: The components folder contains all the “shared components “ that can be used in different modules like Modals, Buttons, Loader, Header, etc.

Example reusable-button :

button.component.ts:

import { Component, OnInit, Input } from '@angular/core';

@Component({
selector: 'app-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.css']
})
export class ButtonComponent implements OnInit {
@Input() label:string;
@Input() functionCall:string;

constructor() { }

@Input() label: string;
@Output() onClick = new EventEmitter<any>();

onClickButton(event) {
this.onClick.emit(event);
}
}

button.component.html:

<div id = "button">
<button type="button" class= "btn" (click)="onClickbutton($event)" >{{label}}</button>
</div>
  • directives: Folder to contains custom directives (alter the appearance of DOM-element, react to user input and how to pass input, etc …)
  • models: all the models across the application that can be divided into sub-folders
|-- models
|-- [+] crud
|-- [+] common
|-- [+] filter
|-- [+] helpers

Core Module: 🎯

|-- core
|-- [+] guards
|-- [+] http
|-- [+] interceptors
|-- [+] mocks
|-- [+] services
|-- core.module.ts
|-- ensureModuleLoadedOnceGuard.ts
|-- logger.service.ts

The CoreModule takes on the role of the root AppModule, which will contain the services folder for (API interactions && Business logics),

Interceptors for adding Authorization to HTTP requests, Caching API requests,

Guards: Custom controls the accessibility of a route based on condition

Short Paths With Path Aliases:➰ ✔️

You should avoid long relative path import like

../../../../../../../../../app/config/config.ts
Or
import { User} from '../../../models/crud/User';

and take advantage of the Typescript aliases which allow us to specify a word/alias for an absolute path in the application form which you can resolve paths from absolutely.

  • Example paths-configuration for your Angular project :
"paths": {"@core/*": ["src/app/core/*"],"@modules/common/*": ["src/app/common/*"],"@shared/*": ["src/app/shared/*"],"@configs/*": ["src/configs/*"],
"@angular/core/src/metadata/*": ["./node_modules/@angular/core"]

And your import will be like this :

import {  Model1 , Model2 , Model3 } from '@shared/models';import { Service1 , Service2 , Service3 } from '@core/service';

Lazy Loading : 📓 📕

As we mention in the first tip about the project structure, If we plan to have a scalable application, you should organize it into modules (Auth Module, Dashboard Module, etc ..) but we should care about application performance!

So one of the techniques to use to make our application load quickly is

Lazy Loading code example 📕:

import { NotFoundComponent } from './modules/general/not-found/not-found.component';

const routes: Routes = [
{ path: '', component: HomeComponent, },
{
path: 'contact',
loadChildren: () => import('./modules/general/contact/contact.module')
.then(mod => mod.ContactModule)
},
{
path: 'about',
loadChildren: () => import('./modules/general/about/about.module')
.then(mod => mod.AboutModule)
},
{
path: 'signin',
loadChildren: () => import('./modules/general/signin/signin.module')
.then(mod => mod.SigninModule)
},
{ path: '**', component: NotFoundComponent }
];

Use Angular CLI for File generating 🔧 🔨:

Angular CLI

Angular CLI is a command-line interface tool that is used to initialize, develop, scaffold, maintain, and even test and debug Angular applications.

So instead of creating the files and folders manually, use Angular CLI to generate new components, directives, modules, services, and pipes.

# Install Angular CLI

npm install -g @angular/cli

# Check Angular CLI version

ng new [project-name]
ng generate component [name] (generate component)
ng g s everything-service (generate service)
ng g pipe my-pipe (generate pipe)
ng g directive my-directive (Generate a directive)
ng g enum some-enum
ng g module fancy-module
ng g cl my-class (Generate a class)
ng g interface my-interface (Generate an interface:)

You can check (https://cheatography.com/wakers01/cheat-sheets/angular-cli/) Angular CLI Cheat Sheet for more details about available commands

Clean TypeScript Code 🍦 🍰:

Typescript is a revolution programming language in web development, it provides a lot of features and APIS, so writing angular code with typescript we should care about clean code tips like :

– Define small functions and limit them to no more than 75 lines

ngOnInit(): void {this.function1);this.function2();}

– Have consistent names for all symbols. The recommended pattern is feature.type.ts. , user.mode.ts ,etc ..

– If the values of the variables are intact, then declare it with ‘const’.

const canvas = chartInstance.chart;

– Use dashes to separate words in the descriptive name and use dots to separate the descriptive name from the type.

– Names of properties and methods should always be in the lower camel case.


//Example
public
isConnected =false;

Always leave one empty line between imports and modules; such as third-party and application imports and third-party modules and custom modules.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { JiraService, MetricsService, NavigationDataService, SubTaskService } from '@core/service';import { Constants } from '@core/utils/Constants';
import { StringUtils } from '@core/utils/string-utils';
import { Responsible, StatusFilterDTO, SubTaskFilter, SubTask, ModelMetricType, NavigationElement } from '@shared/models'
import { SharedData } from '@shared/sharedData';

Use Interface instead of Class for type checking 🚩 🏳:

Interfaces are great when you only need the type checking whereas classes are great when you not only want the type checking, but you need some methods or require some other logic.

Interfaces are contracts. An interface defines what’s inside an object (again … not an instance of a class). When you define your interface

interface MyInterface{
a: number;
b: string;
}

Advantage of Using Interface :

Example Use-Case that you have an object with two properties (a , b)

interface MyObject{
a: string;
b: number;
}

if you don’t respect this contract (interface definition), typescript with throw an error (for example passing the parameter to a function with MyObject instance )

interface MyObject{a: string;b: number;}let obj :MyObject;
const test = (obj: MyObject) =>
{obj.a=100;
//Error "Type 'number' is not assignable to type 'string'."
}

Another advantage of the usage of Typescript for data modeling instead of class as the compiler will not generate any corresponding JavaScript code for an interface at run time ⭕️ 💢

Stop Using ‘any’, There’s a Type For That

Any type is used to represent any JavaScript value.

But writing your typescript code without types, you will add extra code for checking arguments and variables, to make sure that your program works correctly ❌ ,

So By using type: any ❌, you can not control the null and undefined values which can occur exceptions and bugs

let a: any;
let b: Object;

a.nomethod(); // Transpiles just fine
b.nomethod(); // Error: Property 'nomethod' does not exist on type 'Object'.

Safe Operator :

Safe navigation operator ( ?. ) is an Angular template expression operator, used to avoids exceptions for null and undefined values in property paths.

<b>1. </b>
<p>{{personone?.name}} </p>
<p>{{personone?.email}} </p>

Using *ngFor without trackBy 🔧 🔨:

Problem ⛔️:

When passing an array of objects to ‘*ngFor’, the angular directive will draw these elements to the DOM, so if there is any Changement in data collection, API call Angular can’t keep track of items in the collection and has no knowledge of which items have been removed or added.

(DOM manipulations are expensive )

Solution:✔️ For performance reasons, the trackBy feature which allows you to track elements when they are added or removed

@Component({
selector: 'app-movies-list',
template: `
<ul>
<li *ngFor="let movie of moviesArr; trackBy: trackByElement">

</li>
</ul>
`
})
// in your typescript codetrackByElement = (index, item) => {return item.id;}

Use hidden over ngIf for showing/hiding elements 🔑:

Avoid this ❌ :

<div  *ngIf="isEpic">
<span> To be defined</span>
</div>

Try with this ✅:

<div class="col" [hidden]="!isEpic">
<span> To be defined</span>
</div>

Avoid console.log()✂️ 📐 :

Using console.log() statements in your production code could be a bad idea as it will slow down the performance of the app and also logging objects with console.log()

Hiding all the consoles (production mode)

if (env === 'production') {
console.log = function () {};
}

Using pipes instead of methods in templates:

Don’t do this ❌ :

<h1>    Current time is {{ currentTime.substr(0, 2).toUpperCase() }}!  </h1>

Do this ✅:

<h1>    Current time is {{ currentTime | SubtPipe | uppercase}}!  </h1>

Avoid complex computations in the template:

<span  *ngIf="object.name" class="badgeAvatar badge-primary"> {{functionFilter(object.name)?.toUpperCase()}}</span>`https://gist.github.com/akhilanandbv003/fd4f3898bb7b9c36f7b9a8c198e01548```

Smart && Dumb Component 🌚 🌝:

Always you should care about the reusability of your code and take advantage of the web framework, that everything is a component

Putting all the code in one component can cause a problem 📛:

  • Starting to have a significant size.
  • Code readability 📕

Solution :

  • Divide components into Smart and Dumb.
  • Keep components as Dumb as possible.
  • Decide when a component should be Smart instead of Dumb.

So instead of putting all the logic code in one component t, we can split them, but before we should learn the two types of Components :

  • Container Components (Smart component) ♻️:

is concerned with how its internal operations work, API calls, fetch data from services

  • Presentational Components 📥 (Dumb components):

Has no dependency on the rest of the app,

Doesn't mutate data

includes @Input attributes as data providers

import{ Component, Input, OnInit } from ‘@angular/core’;
@Component({
selector: ‘app-book-list’,
templateUrl: ‘./book-list.component.html’,
styleUrls: [‘./book-list.component.css’]
})
export class BookItemComponent implements OnInit {
@Input(‘book’) book: Book;
constructor() { }
ngOnInit() {
}

But!

  • Components should only deal with display logic
  • the business logic should be extracted from in-service methods and separated from the view
  • Business logic is usually easier to the unit test when extracted out to a service, and can be reused by any other components that need the same business logic applied.

Detection Optimizations (Onpush) 🔔 🔕:

AS we discussed in the previous part about the Presentational component which ide depends on the @Input Data can cause a performance issue if we dint choose the right Change Detection strategy for our components!

Problem 🚫: By default, Angular uses the ChangeDetectionStrategy.Default change detection strategy! so every time where there are changes (events, timers, user interactions, etc .. ) a change detection will run on all components.

Solution ✅: We can set the ChangeDetectionStrategy of our component to ChangeDetectionStrategy.OnPush .

This tells Angular that the component only depends on its @inputs() and needs to be checked only in the following cases:

it improves performance by minimizing change detection cycles.

Generic Service Api Interaction (DRY ):

While defining our services in our Angular project, which are dedicated for fetching API resources and dealing with HTTP requests as we see there is a lot of duplicated code

Problem 🚫: Code Duplication

Service-1-code :

@Injectable({providedIn: 'root'})export class Object1Service {constructor(private httpClient: HttpClient) { }public getUsers(page: number, size: number, filter: string): Observable<PageUser> {const url = filter != null ? `${config.serviceBaseUrl}/users/all?&page=${page}&size=${size}&search=${filter}` : `${config.serviceBaseUrl}/users/all?&page=${page}&size=${size}`;return this.httpClient.get<PageUser>(url)}

Service-2-code :

@Injectable({providedIn: 'root'})export class Object2Service {constructor(private httpClient: HttpClient) {  }public getUseCase(epicId: string|number, page: string, size: string, searchFilter: UseCaseSearchFilter): Observable<PageUseCase> {const url = `${config.serviceBaseUrl}/v3/use-cases/by-epic/${epicId}?&page=${page}&size=${size}`;return this.httpClient.post<any>(url, searchFilter);}

Solution ✅: Generic-Service

export class SubResourceService<T extends Resource> {
constructor(
private httpClient: HttpClient,
private url: string,
private parentEndpoint: string,
private endpoint: string,
private serializer: Serializer) { }
public create(item: T): Observable<T> {
return this.httpClient
.post<T>(`${this.url}/${this.parentEndpoint}/${item.parentId}/${this.endpoint}`,
this.serializer.fromJson(item))
.pipe(map((data: any) => this.serializer.fromJson(data) as T));
}

Clear Your Subscriptions 🚫 ❌:

One of the biggest challenges of using Rxjs with Angular is we need to free up resources used by observables to avoid memory leaks

Problem :

Subscribing to each observable is like creating an open connection that is never closed!

Solution ✅:

Clear your subscriptions !!! 💥 💥 💥

Calling the unsubscribe()⚡️ :

Using the takeUntil() operator from RxJs 🔥 :

“ This takeUntil() the operator keeps using a source Observable until the notifier Observable that we pass into the takeUntil() the operator tells it to stop.”

Using AsyncPipe 💥:

the async pipe is used for auto unsubscribing observe

Use appropriate RXJS operators ✅✅ :

RxJS stands for *R*eactive E*x*tensions for *J*ava*S*cript, and it’s a library that gives us an implementation of Observables for JavaScript.

Rxjs is a cool library providing a lot of useful APIS and operators to interact with complex scenarios and which undertakes all the hard work and makes it possible,

So let’ talk about the most common-used Rxjs operators :

switchMap: where you are no longer concerned with the response of the previous request when a new input arrives

import { Component } from '@angular/core';import { FormControl,FormGroup,FormBuilder } from '@angular/forms';import { SearchService } from './services/search.service';import 'rxjs/Rx';@Component({selector: 'app-root',template: `<form [formGroup]="coolForm"><input formControlName="search" placeholder="Search Spotify artist"></form><div *ngFor="let artist of result">{{artist.name}}</div>`})export class AppComponent {searchField: FormControl;coolForm: FormGroup;constructor(private searchService:SearchService, private fb:FormBuilder) {this.searchField = new FormControl();this.coolForm = fb.group({search: this.searchField});this.searchField.valueChanges.debounceTime(400).switchMap(term => this.searchService.search(term)).subscribe((result) => {this.result = result.artists.items});}}

mergeMap: This operator is best used when you wish to flatten an inner observable but want to manually control the number of inner subscriptions.

this.userService.getCurrentUser()
.pipe(
tap((currentUser) => (this.userId = currentUser._id)),mergeMap((currentUser) =>this.blogsService.getBlogsByUser(currentUser._id)));

forkjoin: There are some use-aces when we cant to wait for multiple HTTP API calls and wait until we got a response, so ‘forkJoin’ is the easiest way to do that :

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { forkJoin } from 'rxjs'; // RxJS 6 syntax
@Injectable()
export class DataService {
constructor(private http: HttpClient) { }public requestDataFromMultipleSources(): Observable<any[]> {
let response1 = this.http.get(requestUrl1);
let response2 = this.http.get(requestUrl2);
let response3 = this.http.get(requestUrl3);
// Observable.forkJoin (RxJS 5) changes to just forkJoin() in RxJS 6
return forkJoin([response1, response2, response3]);
}
}

Caching API Call ⚠️ 🚸:

There are some use-cases when making API calls, the data dot not change frequently, So we can add a caching mechanism for these APIS calls,

that’s will helps us to avoid unnecessary API calls as a result improve the speed of our Application 💰.

For the Angular project, there are a lot of technical implementations for this caching mechanism

Solution 1 ✔ shareReplay ️Rxjs operator :

It will always return the last emitted value from the data stream!

Use CDK Virtual Scroll 📜 📃:

Problem 🚫: Modern web applications are complex. They have a lot of moving parts, complexity and oftentimes they have to deal with a huge amount of data.

So if we are dealing with a very common use case: displaying the list of users

As the number of items in the list increase so does the number of elements in DOM resulting in more memory and CPU usage.

Solution : @angular/cdk

“The <cdk-virtual-scroll-viewport> displays large lists of elements performantly by only rendering the items that fit on-screen. “

adding the @angular/cdk package to our project:

$ npm install @angular/cdk// in your app.module.tsimport { ScrollingModule} from '@angular/cdk/scrolling';
imports: [
ScrollingModule
]

// Component
export class CdkVirtualScroll {
items = [];
constructor() {
this.users = Array.from({length: 100000}).map((_, i) => `scroll list ${i}`);
}
}

compoennt.html :

//Template

<ul>
<cdk-virtual-scroll-viewport itemSize="100">
<ng-container *cdkVirtualFor="let user of users">

<li> {{user}}</li>

</ng-container>
</cdk-virtual-scroll-viewport>
</ul>

State Management with Ngrx 🎴:

Nowadays, we are dealing with complex applications and complex business needs, a rich complex projects like Facebook, Twitter, etc have the need for state Management and an architecture for their front-end projects

For Angular Developers, their good libraries that make it easy to add state management for our application like NGRX

What benefits of using state management :

  • It enables sharing data between different components
  • It provides centralized control for state transition
  • The code will be clean and more readable
  • Makes it easy to debug when something goes wrong
  • Dev tools are available for tracing and debugging state management libraries

For small applications 🏍 , we can use @input, @Output, and EventEmitter for the component communication.

For small to mid-level applications 🚜 use, RxJS shared services for the feature module communication. Use RxJS BehaviourSubject and ReplaySubject to hold the data and share the data between components and modules.

For large and complex applications 🚎 that need to maintain state. It is good to build a store. Redux or NGRX provides a store for Angular applications.

Conclusion ❤️ 💛:

In conclusion, my advice for junior software developers with any technology to look always how to write a clean code and search about the best practices for it, not only make it functional just working, because this is the thing that will make you advance quickly in your career

References :

<script>alert('try your best')</script>

<script>alert('try your best')</script>