This blog describes some practices which can be followed in an Angular project to make the code more consistent, readable and extendable. The same can be applied to other frameworks. These are sort of recommendations instead of ‘must have’. Let’s Go..!
Most often ngOnInit lifecycle hook is used to initialize component properties, to make HTTP requests and to subscribe to some state changes.
1import { Component, OnInit, Input } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { FormControl } from '@angular/forms';
4
5@Component({
6 selector: 'app-demo',
7 templateUrl: './demo.component.html',
8 styleUrls: ['./demo.component.scss']
9})
10export class Demo implements OnInit {
11
12 tempProperty: string;
13 tempData: any;
14 tempFormControl: FormControl;
15
16 constructor(private httpClient: HttpClient) {
17 this.tempFromControl = new FormControl(null);
18 }
19
20 ngOnInit() {
21 this.tempProperty = 'Hello World!';
22
23 this.httpClient.get('https://apiendpoint/').subscribe(
24 (response) => {
25 this.tempData = response;
26 }
27 );
28
29 this.tempFormControl.valueChanges.subscribe(
30 (value) => {
31 console.log(value);
32 }
33 );
34 }
35}
There are 3 variables tempProperty, tempData and tempFormControl. All of these variables have different usage. The above code works perfectly fine. Also, as in our example, the component has no logic and is really small. In real-world scenarios, a component can be very large in terms of lines of code and there can be more code in the ngOnInit method. As you can see in ngOnInit method of our Demo-Component, all the operations are performed in the same method. It can be divided in some way, like below.
1import { Component, OnInit, Input } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { FormControl } from '@angular/forms';
4
5@Component({
6 selector: 'app-demo',
7 templateUrl: './demo.component.html',
8 styleUrls: ['./demo.component.scss']
9})
10export class Demo implements OnInit {
11
12 tempProperty: string;
13 tempData: any;
14 tempFormControl: FormControl;
15
16 constructor(private httpClient: HttpClient) {
17 this.tempFromControl = new FormControl(null);
18 }
19
20 ngOnInit() {
21 initialization();
22 getData();
23 makeSubscriptions();
24 }
25
26 initialization() {
27 this.tempProperty = 'Hello World!';
28 }
29
30 getData() {
31 this.httpClient.get('https://apiendpoint/').subscribe(
32 (response) => {
33 this.tempData = response;
34 }
35 );
36 }
37
38 makeSubscriptions() {
39 this.tempFormControl.valueChanges.subscribe(
40 (value) => {
41 console.log(value);
42 }
43 );
44 }
45}
We will now divide our ngOnInit method into three different methods.
This approach makes code more readable and extendable. It is really easy for new developers to understand where all the component properties are set, where all the subscriptions are defined. Also, if a new developer wants to add a new property or want to subscribe to a new observable it is easy for him/her to add it.
Check out the code below.
1import { Component, OnInit, Input } from '@angular/core';
2import { Subject } from 'rxjs';
3
4@Component({
5 selector: 'app-demo',
6 templateUrl: './demo.component.html',
7 styleUrls: ['./demo.component.scss']
8})
9export class Demo implements OnInit {
10
11 statusUpdate = new Subject<boolean>();
12 categoryUpdate: Subject<string>;
13 userUpdate: Subject<number>;
14
15 constructor() {
16 this.categoryUpdate = new Subject<string>();
17 }
18
19 ngOnInit() {
20 this.userUpdate = new Subject<number>();
21 }
22
23}
You might have recognized the inconsistency in the above snippet. There are 3 objects, statusUpdate, categoryUpdate and userUpdate.
statusUpdate is instantiated at declaration time, categoryUpdate is instantiated in the constructor() and userUpdate is instantiated in ngOnInit() method.
There are two problems here:
1import { Component, OnInit, Input } from '@angular/core';
2import { Subject } from 'rxjs';
3
4@Component({
5 selector: 'app-demo',
6 templateUrl: './demo.component.html',
7 styleUrls: ['./demo.component.scss']
8})
9export class Demo implements OnInit {
10
11 statusUpdate: Subject<boolean>;
12 categoryUpdate: Subject<string>;
13 userUpdate: Subject<number>;
14
15 constructor() {
16 instantiation();
17 }
18
19 instantiation() {
20 this.statusUpdate = new Subject<boolean>();
21 this.categoryUpdate = new Subject<string>();
22 this.userUpdate = new Subject<number>();
23 }
24
25}
Now, there is only one method to instantiate all the objects of the component. The same can be implemented in the case of Services. It is not only to instantiate Subject but also can be used to instantiate FormControl, FormGroup or any other classes.
Consider the following scenarios:
You define your path to the Login page as /login. This path is used everywhere in the application. For example,
How would you do that? You have to go to every single file and change the path just because you introduced a new word ( user ) in your path.
Create a file named route.constants.ts and define every single path as constants.
1// URLS
2
3export const LOGIN_URL = 'login';
4export const REGISTER_URL = 'register';
5export const FORGOT_PASSWORD = 'forgot-password';
1import * as routeConstants from 'route.constants.ts';
2
3const routes: Routes = [
4 {
5 path: routeConstants.LOGIN_URL,
6 component: LoginComponent,
7 },
8 {
9 path: routeConstants.REGISTER_URL,
10 component: RegisterComponent,
11 },
12 {
13 path: routeConstants.FORGOT_PASSWORD_URL,
14 component: ForgotPasswordComponent,
15 }
16];
You can use the same constants everywhere you want to redirect a user to the Login page. If you want to change the path, you just have to modify the value of a constant variable LOGIN_URL.
It is not only for paths of routes. You can use constants for your unique modal-ids, default configuration options of any library ( Date-picker, Charts, Maps ).
Consider the following scenarios:
You have a method in the service which has a signature as below:
1getStatus(): 'todo' | 'in-progress' | 'done' {
2 // some logic to return status
3}
You use this method in your component as below:
1const status: string = someService.getStatus();
You want your getStatus() method to return one of the statuses only from todo, in-progress and done. Therefore, you use union types.
But in your component, you want to assign the value returned by getStatus() method to a variable having string type. It will give the error as:
1Type "'todo' | 'in-progress' | 'done'" is not assignable to type 'string'.
You might use the solution of using as to explicitly convert the return type as below:
1const status: string = someService.getStatus() as string;
But if you have to use getStatus() in many components, the above solution is not a better one.
Define an enum as below:
1export enum Status {
2 Todo = 'todo',
3 InProgess = 'in-progess',
4 Done = 'done'
5}
Modify the signature of getStatus():
1getStatus(): Status {
2 // some logic to return status
3}
Modify the usage of getStatus():
1const status: Status = someService.getStatus();
It resolves the error and you can be sure that there are only three types of statuses returned by the method.
Also, read our blog on dynamically adding/pushing and removing form fields to FormArray using reactive forms in Angular.