How to Write Better Code in Angular

Learn how to write clean, maintainable, scalable, performance-optimized and testable code in Angular

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces

--

Angular abstracts so many complexities but, still, our app can become difficult to test and maintain over time if we don’t make sure to write good code. In this article, we will look at different ways to write better code in Angular.

Caching/Memoization

Writing fast code is good but writing faster code is bae. Your users would exit from your site if it takes ~3 mins to load. We should aim to write code that executes very fast.

The thing I have learned to write fast code is to know how the execution engine executes a piece of codes and how a pattern of writing might influence its execution rate.

Now, caching or memoization is one of the oldest tricks in the book, this entails saving the initial inputs and returning when they occur again. This improves the performance of the app as the app would not evaluate the same inputs over and over.

Optimize template expressions

Template expressions are a piece of executable code supported by Angular. They are between the double curly braces {{}}. Template expressions can be:

  • Binary expressions
  • Function calls
  • Method calls
  • Primary expressions

Whenever the view of a component is rendered the template expressions are evaluated and their results are appended to the DOM using interpolation.

If we run heavy template expressions, template expressions like method/function calls that take longer to execute.

@Component({
// ...
template: `
<div>
{{longMethod()}}
</div>
`
})
export class App {
longMethod() {
// takes 3 mins to exec
return 67
}
}

The longMethod method is called in the template. This method is expensive, it takes 3mins to execute. That means it will take 3 mins for users to see something on their browser. This is bad for UI experience. Expressions should finish quickly. How do we write better code to make the UI experience good?

1. Finish quickly

Here, we should refactor the code to finish quickly. Since it executes under 3 mins we should pull in best practices in JS to make sure the code executes in lesser time. Or we can make the code execute in batches like how React fiber works. Pausing the rendering working to begin when the browser is idle.

2. Use web workers

Sometimes we cannot refactor the functions/method to run in lesser time, but we cannot let users experience UI drag. What will be left is to move the code to another thread other than the browser thread. This is done using Web Workers.

A web worker is a thread created to run heavy computations that will drag when running on a browser. The web workers run the code and emit the result to the browser. So the offloading of the heavy work to web workers leave the browser and the UI experience smooth.

3. Cache the template expressions

This option brings about caching the template expressions. We will store the results of the original call with an input then, when the function/method is called again with the same inputs there will be no execution of the function/method, the result will be returned from cache.

To make this thing feasible, the function must be:

  • pure function/method: pure means it doesn’t depend or affect any external variable. If it does it will be unpredictable and hard to cache.

4. Use pipe

Some people argue that we should use pipe instead of functions in template. This is good because we have the option of memoizing our pipes.

App Shell

This is a kind of way to make your app seems to load faster. It requires making the app shell load first from the cache before the app logic and the rest of the UI loads from the network. App shell is the minimum UI that the users see, this indicates that the main content will be delivered soon.

Shareable/Reusable Angular components

This is the core of writing better code. Angular is composed of components. It starts from the root component to the components in branches. Modular components can and should be shared and reused to build your apps.

Use open-source tools like Bit (GitHub) to share components between applications, and create a reusable collection of components.

Bit encapsulates components you write with all their files, dependencies and setup. Then, it lets you instantly reuse them between different applications so you don’t have to re-invent the wheel or work hard to share the code.

Share Angular components in bit.dev; run them anywhere

Your team can collaborate to discover your shared components in bit.dev, and use them right in any new application- and they will run out-of-the-box. You can even develop and update components right from any new project.

This means you can turn components into modular building blocks you and your team can collaborate on, to build modular applications faster together.

Container and compositional components

Angular was designed to promote code re-use and separation of concerns. This design-pattern makes our code easier to re-use, maintain, scale, optimize and test. As a result, the components in Angular can be divided into two Container and Presentational components.

Container components interact with Servies or Store to get data. This data can either be rendered or passed down to its children components. They are also called smart components because they are smart enough not to wait for data but to go for it.

Presentational components are meant for UI presentation only. They are dumb components because they are fed with data, they don’t go for it. If not fed with any data they display nothing.

Presentational components are re-usable and since they are re-usable they can also be shared across different projects. Why are Presentational components shareable and re-usable? It is because they are not tied to the app they are used within. Container components are not reusable because they are tied to the business logic of the app they are used. Business logic can’t be shared and reused because they are specific and tied to projects.

They deal with only display logic, display logic deals with how re-structure the DOM. Shareable components work in any environment or context they find themselves in. UI libraries (ngBootstrap, Material UI, Angular Material) are filled with presentational components eg Buttons, Tables, Tabs, Modals, Labels, Alerts, Panels, etc.

When building apps, we should follow this design pattern postulated by Jack Tomaszewski:

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

No logic in templates

Adding logic to templates makes it hard to test. We all know that templates are the view that contains HTML string that will be rendered on the browser.

Let’s say we have this:

@Component({
...
template: `
<div>
<h3>My List</h3>
<div class="alert alert-danger" *ngIf="logged == true">You are not allowed here</div>
...
<div class="panel">
<div class="panel-header">{{name == 'undefined' ? "John Doe" : name}}</div>
...
</div>
</div>
`
})
export class App {
logged: boolean = false
name: string = null
//...
}

This will be very hard to test. To make it better we will have to remove the logic from the template:

@Component({
...
template: `
<div>
<h3>My List</h3>
<div class="alert alert-danger" *ngIf="logged">You are not allowed here</div>
...
<div class="panel">
<div class="panel-header">{{name}}</div>
...
</div>
</div>
`
})
export class App {
logged: boolean = false
name: string = "John Doe"
//...
}

This is easier to test. The class is the controller, so it should have all the display logic. Template should only display based on the class property values.

Unsubscribe in ngOnDestroy

RxJS brought reactive programming to us. It was adopted by Angular, and now hugely used in the framework. This involves observers subscribing to data streams.

Best practices tell us to always unsubscribe from subscriptions to avoid memory leaks. Angular provides us with a lifecycle hook that is called every time the component is being destroyed. It is ngOnDestroy, it is called when the destruction of a component is initiated. That's it is called before a component is removed from the component tree.

You see, this lifecycle is the best place to clean up our subscriptions.

@Component({
// ...
template: `
<div *ngFor="let list of lists">
//...
</div>
`
})
export class App implements OnInit {
private listObservable: Observable
ngOnInit() {
listObservable = this.listService.getAllLists().subscribe(res=> this.lists = res)
}
}

See if we forget to unsubscribe the listObservable it will be left open and as we said it will introduce inconsistencies and bugs to our app. So the better code is this:

@Component({
// ...
template: `
<div *ngFor="let list of lists">
//...
</div>
`
})
export class App implements OnDestroy, OnInit {
private listObservable: Observable
ngOnInit() {
listObservable = this.listService.getAllLists().subscribe(res=> this.lists = res)
}
ngOnDestroy() {
this.listObservable.unsubscribe()
}
}

We make our component implement OnDestroy and we add the ngOnDestroy, there we unsubscribed from the listObservable.

Use trackBy function for *ngFor

*ngFor is a built-in structural directive that is used for repeating a template over an array of data passed to it. Now, what is the problem with this? Let's take it like this.

Angular uses the === reference operator to determine whether to re-render a component based on its inputs. Now, ngFor takes in the array it is to iterate via ngForOf input, whenever we push or pop items from the items Angular re-renders the ngFor directive updating the view re-rendering the whole DOM tree. If we have a huge collection in the array, we will see that a small push/pop in the array will cause the whole collection to be re-rendered on the DOM. This will cause a huge performance impact. ngFor needs a way to tell it which elements were added or removed so it won't re-render the entire collection.

Angular introduces the Differs, which is used to determine added items, removed items, changed items in an array also it has a way to tell it how to track items using the trackBy function. This trackBy function tells Differs the property in the objects inside that array to track added/removed/changed items. trackBy takes a function which has two arguments: index and item. If trackBy is given, Angular track changes by the return value of the function.

Now, if we have our component like this:

@Component({
// ...
template: `
<div *ngFor="let movie of movies">
...
</div>
`
})
export class App {
private movies: Movies[]
constructor(){
this.movies = [
{
id: 0,
name: 'nnamdi'
},
{
id: 1,
name: 'feynman'
},
// ... 1000 items
]
}
}

If we add an item to the movies array

this.movies = this.movies.concat([
{
id: Date.now(),
name
}
])

The ngFor directive will be re-rendered with the new movies array and it will re-render the whole movies array that will be huge just for one addition. If we want to write a better code to avoid this performance issue, we will introduce the trackBy function:

@Component({
// ...
template: `
<div *ngFor="let movie of movies; trackBy: trackByFn">
...
</div>
`
})
export class App {
private movies: Movies[]
constructor(){
this.movies = [
{
id: 0,
name: 'nnamdi'
},
{
id: 1,
name: 'feynman'
},
// ... 1000 items
]
}
trackByFn(inde, item) {
return item.id
}
}

See we added trackBy: trackByFn to our template in the *ngFor directive. The trackByFn tells Differ to track changes in our movies array using the id property. Now if we add or remove item(s) from the movies array, Angular won't render the whole DOM tree but only the items that changed.

Use pipes

Pipe is a way of transforming a stream of data without affecting the originial source.

Pipes in Angular is used in a template to transform data from the component without changing it.

@Component({
template: `
{{dataFromTemplate | myPipe}}
`
})

Pipe is denoted with the @Pipe decorator. The metadata contains this:

{
name: string,
pure: boolean
}

The name indicates the name of the string, the pure option tells Angular whether to memoize the inputs of this Pipe or not. Setting the pure flag to true would memoize the Pipe.

Memoization of Pipes would improve the performance of our component because the transformation algorithm of the Pipes would only be re-run when the inputs change.

use async pipe in template

Best practices encourage us to use async pipes whenever we are using Observables and want to display the value in the template. This is because if we subscribe to observables in our components we are likely to forget to unsubscribe from the data stream when the component is being destroyed or the page is being navigated away from.

This would lead to memory leak and would lead to open subscriptions.

@Component({
// ...
template:`
<div>
{{movies}}
</div>
`
})
export class App implements OnInit {
private movies: Movies[]
constructor(private store: Store) {}
ngOnInit() {
this.store.select(state=> state.movies).subscribe(data= this.movies = data)
}
}

See we subscribed to the Store to select the movies slice from the state object. Then we assigned the movies slice to the movies property which we display in the template.

We are most likely to forget to unsubscribe from the Store and when the component is destroyed there will be open subscription.

It will better if we write better code by introducing the async pipe:

@Component({
// ...
template:`
<div>
{{movies$ | async}}
</div>
`
})
export class App implements OnInit {
private movies$: Observable<Movies[]>
constructor(private store: Store<Movies[]>) {}
ngOnInit() {
this.movies$ = this.store.select(state=> state.movies)
}
}

Now there is no need of adding the ngOnDestroy lifecycle to destroy our Observable. async pipe does it for us automatically.

Change Detection

This is one of the powerful features in Angular. This is used to detect when changes might have occurred in the components so re-render is run throughout the component tree. Change detection detects when data might have changed, Angular sniffs this out through the use of Zone.js. Zone.js is a library that monkey-patches all the browser APIS, so when one is run in our browser Angular deduces data might have changed so it re-renders the Component tree to reflect the changes.

The browser APIs it monkey-patches are:

  • XMLHttpRequest
  • DOM events like mouse movements, click events
  • setTimeout, setInterval, clearInterval, setImmediate etc

Detach component from component tree

Each component in the component tree has a change detector attached which forms a change detector tree. Each component can detach itself from the component tree. Each component has a flag that Angular uses to know which component to run CD on. While Angular traverses the CD tree, it only triggers CD on components whose has its CD flag set to true.

This has a performance bottleneck when we are running an expensive function and it continuously triggers re-renders. This might cause drag in the UI which is bad for users. So to stave off that we have to detach the component from the CD tree and attach it back when we want to display the data.

Imagine we have an app that its value changes constantly:

@Component({
// ...
template: `
<div>
{{data}}
</div>
`
})
export class Comp {
private data
constructor(private dataService: DataService) {}
getData() {
this.data = this.dataService.loadData()
}
}

To write a better code that will make this component optimal, we will have to detach the component from the Cd tree. To do it we will inject the ChangeDetectorRef and call the detach method. We will load data from the DataService#loadData for every 5 seconds.

@Component({
// ...
template: `
<div>
{{data}}
</div>
`
})
export class Comp {
private data
constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) {
this.changeDetectorRef.detach()
setInterval(()=> {
this.changeDetectorRef.detectChanges()
}, 5000)
}
getData() {
this.data = this.dataService.loadData()
}
}

See we called detach that removed Comp from the CD tree. Then we used the setInterval to run re-render on the component for 5 secounds.

Run outside Angular zone

Angular extended the NgZone class in Zone.js to add more useful APIs. One is the runOutsideAngular API. This is used to optimize performance when starting a work consisting of one or more asynchronous tasks that don't require UI updates or error handling to be handled by Angular.

When done with such tasks we can re-renter the Angular zone by calling another API run.

constructor(private ngZone: NgZone) {}    this.ngZone.runOutsideAngular(()=> {
// run outside Angular zone
setTimeout(()=> {
this.ngZone.run(()=> {
// run inside ngZone
})
}, 5000)
})

ChangeDetectionStrategy.OnPush

Angular introduce this to memoize UI-based components. This one of the most used memoization techniques to boost the performance of our apps.

OnPush strategy is used to set the Checked flag of a component so it not checked on subsequent re-renders unless its inputs referentially change.

For example, if we have a movies app like this:

@Component({
// ...
template: `
<div *ngFor="let movie of movies">
...
</div>
`
})
export class MoviesList {
@Input() movies
}

It is fed with movies via the @Input() movies. Now, when the parent component, as the MovieList is its child it will also be re-rendered. What if in a case where the @Input() has not changed, we will see it is futile to re-render the component doing so would present a huge performance setback, in a case where we have a huge movie collection.

To make this component performant, we need to set its Cd strategy to OnPush, all component by default has CD strategy set to Default which means the component will always be re-rendered on every CD cycle.

@Component({
// ...
template: `
<div *ngFor="let movie of movies">
...
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MoviesList {
@Input() movies
}

With this, the MovieList component will only render on the startup/bootstrap stage then, the Checked flag will be set to true. So the subsequent re-renders it will be skipped unless the inputs have changed.

Conclusion

The list in this post aims to improve your codebase, you will find it more useful when working in a team. You will find yourself seamlessly writing and merging codebases without re-writing any part to fit in.

Bear in mind, you will not implement all this all when building apps initially you will find it hard. But with constant practice and application, you will find yourself following all these practices to the last.

Thanks!!

Related Stories

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕