Contents

5 Simple Improvements You Can Make To Your AngularJS Code

5 Simple Improvements You Can Make To Your AngularJS Code

Use this list when you review and refactor your Angular codebase.

https://cdn-images-1.medium.com/max/800/1*Y89mqkq37iZk9-eyEQo2Vw.png

Having built and maintained several Angular apps, there are a few recurring themes I encounter when I rewrite code or review a pull request. I’ll go through five of them and share what we can do about them. I believe most of us are going to encounter similar patterns and potential improvements.

1. Automatically unsubscribe observables

It may seem obvious that we should always unsubscribe from what we subscribe to. However, we can still forget to unsubscribe sometimes. Incomplete observables or unsubscribed subscriptions cause memory leaks. Memory leaks slow down your Angular app.

Instead of calling unsubscribe() for every subscription, we should handle this automatically to prevent missing unsubscribe() calls.

What we can do

  1. If we only need to retrieve the value once, use first() or take(1).
const countries$ = ['Canada', 'United States', 'Vietnam', 'Philippines'];
countries$.pipe(take(1)).subscribe(countries => console.log(countries));
  1. If you are already using Angular 16, use the takeUntilDestroyed() operator. It will automatically complete your Observable, ultimately unsubscribing any subscriptions if there are any.

  2. If you are using older versions before Angular 16, you can complete or “destroy” your subscription in ngOnDestroy(). This has the same effect as takeUntilDestroyed() but with extra boilerplate code.

private destroyed$: ReplaySubject<boolean> = new ReplaySubject(1);

ngOnInit() {
    this.searchReposFormControl.valueChanges
        .pipe(takeUntil(this.destroyed$))
        .subscribe((searchString) => {
            if (searchString) {
                this.searchType = SearchType.REPOS;
                this.searchSubject$.next(searchString);
            }
        });
}

ngOnDestroy() {
    this.destroyed$.next(true);
    this.destroyed$.unsubscribe();
}

I wrote a blog post that dives deep into this: How to Automatically Unsubscribe Multiple Observables in Angular.

2. Avoid passing observables as parameters

It can be tempting to pass the observable as an input parameter to the child component. When we do this, the child component is now responsible for subscribing and unsubscribing to the observable. This will potentially add more complexity and repeat code.

<child-component [inputParameter]="myObservable$"></child-component>

What we can do

Instead of passing the responsibility of subscribing to retrieve the observable’s results to the child component, we can use the async pipe so the child component will receive the value directly.

<child-component [inputParameter]="myObservable$ | async"></child-component>

The child component doesn’t need to subscribe to the observable that the parent passed. The async pipe also simplifies our code. By adding an async pipe call, the child components will not require any extra “subscribe” and “unsubscribe” calls.

3. Avoid function calls in templates

Except for event binding or triggers, Angular’s change detection by default will attempt to execute the functions in its template multiple times.

For example, if you have the following function call in my template:

<div>
    {{ formatAddress(address) }}
</div>

And you add a console log in the formatAddress() function:

formatAddress(address: string): void {
    console.log('formatting address...');
    // do stuff
}

It will print the “formatting address…” string multiple times in your web dev tools console.

What we can do

  1. We can assign the function call’s return value to a public property. e.g.
myFormattedAddress = this.formatAddress(address);

The template can use the public property to display its value:

<div>
    {{ myFormattedAddress }}
</div>
  1. We can’t always assign the function call to a public property. One example is if we need to format the address for every row value in a table, we can’t just assign the function’s return value to a public property. Here, use pipes instead of calling the function directly from the template. Unlike template function calls, pipes will not execute multiple times.

Convert the component method to a pipe:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'formatAddress'})
export class FormatAddressPipe implements PipeTransform {
    transform(address: string): string {
        // some code to format address
    }
}

Use the pipe in the template:

<div>
    {{ address | formatAddress }}
</div>

Learn more aboutAngular Pipes.

4. Too many parameters (especially in pipes)

This can also apply to any function or method. However, it gets worse with pipes because of its syntax. Pipes can be harder to follow in your template if we pass too many parameters to it.

The pipe:

import { Pipe, PipeTransform } from '@angular/core';

// Returns a formatted fighter profile
@Pipe({ name: 'fighterProfile' })
export class FighterProfilePipe implements PipeTransform {
    transform(name, wins, losses, weight, height): string {
        // do stuff
    }
}

In our template:

<div> {{ 'Khabib Nurmagomedov' | fighterProfile: 29: 0: 155: 178 }}</div>

The example above can be difficult to follow at first glance.

What we can do

Use the parameter object pattern.

Wrap the parameters into a single object. That single object is now the parameter that we pass to our pipe’s transform method:

import { Pipe, PipeTransform } from '@angular/core';

export interface FighterProfile {
    name: string;
    weight: number;
    height: number;
    wins: number;
    losses: number;
}

// Returns a formatted fighter profile
@Pipe({ name: 'fighterProfile' })
export class FighterProfilePipe implements PipeTransform {
    transform(fighterProfile: FighterProfile): string {
        // do something
    }
}

Pipe implementation in our template is more readable this way:

<div>{{ { name: 'Khabib Nurmagomedov', wins: 29, losses: 0, weight: 155, height: 178 } | fighterProfile }}</div>

I first learned about this pattern fromClean Code: A Handbook of Agile Software Craftsmanship. Clean Code even suggests that three or more parameters are too much for a function.

More about the Parameter Object Pattern in the blog post:Why I Prefer to Use the Parameter Object Pattern in Angular Pipes

5. Make use of firstValueFrom()

We’d typically usetake(1)orfirst()if we only need to take the first value of an observable and we don’t care about the succeeding values

printObservable(): void {
    myObservable$.pipe(take(1)).subscribe(res => {
        console.log(res);
    });
}

We can avoid having to use subscribe by using a combination ofasync/awaitandfirstValueFrom().

What we can do

If we can convert the method to async e.g., it’s a method triggered directly from the event in the template, we could usefirstValueFrom(). We will also need to convert the return type to a promise.

import { firstValueFrom } from 'rxjs';

async printObservable(): Promise<void> {
    const res = await firstValueFrom(myObservable$);
    console.log(res);
    // Do something with the result.
}

Conclusion

There are different ways to write your code. It’s always best to opt for the simplest way to achieve the same results. The simpler, the more readable your code can be, the less cognitive load it requires other developers to read your code. You’ll end up with a maintainable code repository.