Skip to content

Angular guide for teams that look for consistency through best practices

Notifications You must be signed in to change notification settings

avivharuzi/angular-best-practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Angular Best Practices

Angular

Angular guide for teams that look for consistency through best practices.

Table of Contents

  1. Single Responsibility Principle
  2. Follow Consistent Angular Coding Styles
  3. Keep Up to Date
  4. Strict Mode
  5. Use Angular CLI
  6. Use State Management
  7. Use Environment Variables
  8. Divide Imports
  9. Prefer Observables Over Promises
  10. Component Properties and Methods Order
  11. Lifecycle Hooks Interfaces and Order
  12. Write Logic Outside Lifecycle Hook
  13. Component Event Names Rules
  14. HTML Wrapping and Order
  15. Wrap Pipes Within Parenthesis
  16. Avoid Logic in Templates
  17. Prevent Memory Leaks
  18. Subscribe in Template Using async Pipe
  19. Use Change Detection OnPush
  20. Avoid Having Subscriptions Inside Subscriptions
  21. Use trackBy Along With ngFor
  22. Strings Should Be Safe
  23. Avoid any Type
  24. Use Mandatory Inputs
  25. Do Not Pass Streams to Components Directly
  26. Do Not Pass Streams to Services
  27. Do Not Expose Subjects
  28. Handle RxJS Errors
  29. Avoid Changing the DOM Directly
  30. Avoid Computing Values in the Template
  31. Use Immutability
  32. Safe Navigation Operator in HTML Template
  33. Sanitize Untrusted Values
  34. Use InjectionToken for Dependency Injection
  35. Prevent Importing Module in Feature Modules
  36. Break Down Into Small Reusable Components
  37. Use Smart and Dumb Components
  38. Use Lazy Loading
  39. Use index.ts
  40. Isolate API Hacks
  41. Cache API Calls
  42. Provide Private Services
  43. Avoid Risky Angular APIs
  44. Avoid Poorly Structured CSS
  45. Lack of Meaningful Unit Tests
  46. Avoid Useless Code Comments
  47. Remove Unused Code
  48. Avoid Using Third-Party Libraries
  49. Base Component Classes
  50. Do Not Remove View Encapsulation
  51. Analyze the Bundle Size
  52. Use CSP To Prevent XSS
  53. Use ECMAScript Features
  54. Use Reactive Forms
  55. Use CDK Virtual Scroll
  56. Use Angular Service Workers and PWA
  57. Use Angular Universal
  58. Use Lint Rules
  59. Use Storybook
  60. Use Angular DevTools Chrome Extension

Single Responsibility Principle

It is very important not to create more than one component, service, directive… inside a single file. Every file should be responsible for a single functionality.

Why?: By doing this, we are keeping our files clean, readable, and maintainable.

Back to top

Follow Consistent Angular Coding Styles

Here are some set of rules we need to follow to make our project with the proper coding standard.

  • Limit files to 400 Lines of code.
  • Define small functions and limit them to no more than 75 lines.
  • Have consistent names for all symbols. The recommended pattern is feature.type.ts.
  • If the values of the variables are intact, then declare it with const.
  • Use dashes to separate words in the descriptive name and use dots to separate the descriptive name from the type, for example: movie-list.component.ts.
  • Names of properties and methods should always be in lower camel case.
  • Always leave one empty line between imports and modules, such as third party and application imports and third-party modules and custom modules.

For more information read it on Angular coding style guide

Back to top

Keep Up to Date

Angular follows semantic versioning with a new major version released every six months.

Semantic versioning is a convention used for versioning software. It has a major.minor.patch format. Angular increments each part when they release major, minor, or patch changes.

You can follow the news about the latest version of Angular from the CHANGELOG and make sure you keep your Angular version up to date, ensuring you always get the latest features, bug fixes, and performance enhancements like Ivy.

It would help if you also used this official tool when updating your project from one version to the next.

Back to top

Strict Mode

The Angular team has moved to apply the strict mode progressively with an option in Angular 10.

From Angular 12 all projects are starting with strict mode enabled by default.

You should check your project is using strict mode and if not update it to use strict mode.

Why?: You’ll benefit from the TypeScript’s type system in your templates and gain best practices rules from Angular team.

{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}

Back to top

Use Angular CLI

Angular CLI is one of the most powerful accessibility tools available when developing apps with Angular. Angular CLI makes it easy to create an application and follows all the best practices! 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, pipes, etc.

# Install Angular CLI
npm i -g @angular/cli

# Check Angular CLI version
ng version

Back to top

Use State Management

One of the most challenging things in software development is state management. State management in Angular helps in managing state transitions by storing the state of any form of data. In the market, there are several state management libraries for Angular like NGRX, NGXS, Akita, etc. and all of them have different usages and purposes.

We can choose suitable state management for our application before we implement it.

Some benefits of using state management.

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

Back to top

Use Environment Variables

Angular provides environment configurations to declare variables unique for each environment. The default environments are development and production. We can even add more environments, or add new variables in existing environment files.

Use it when you depend on different values on different environments.

Development environment (the default).

environment.ts

export const environment = {
  production: false,
  baseApiUrl: 'http://localhost:8080',
};

Production environment.

environment.prod.ts

export const environment = {
  production: true,
  baseApiUrl: 'https://api.example.com',
};

Back to top

Divide Imports

Keeping your file imports ordered and neat is challenging, especially when using an IDE to auto-add them as you type. As your files grow, they tend to get quite messy.

It's good practice dividing the imports by these types:

  1. Angular imports always go at the top
  2. RxJS imports
  3. Third parties (non-core)
  4. Local/Project imports at the end

It’s also a good practice to leave a comment above each group, but it's not required unless there is a lot of imports.

// Angular.
import { ChangeDetectionStrategy, Component } from '@angular/core';

// RxJS.
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';

// Third Parties.
import { MatDialog } from '@angular/material/dialog';

// Local.
import { AuthFacade } from '@my-project/auth';

Back to top

Prefer Observables Over Promises

Observables partly overlaps the standard JavaScript Promise functionality. Both are meant to handle asynchronous code. However, Observables can do so much more than Promises. Observables can resolve to more than just one value, because they are a stream of values. Observables are everywhere in Angular Framework. We can see that, by looking at the Angular HttpClient. It returns Observables, even when it is clear, that a HTTP call can never result in more than one response. Mixing Observables with Promises isn't a good solution. That way, we have completely different implementations, that are hardly compatible with each other in different parts of our application.

Back to top

Component Properties and Methods Order

  • Add public and private properties above the constructor
  • Add public and private methods right below the constructor or if you have life cycle hooks so after them
  • Locate the properties and methods with the same decorator close to each other and write empty line in between groups of properties with the same decorator.
export class MyComponent implements OnInit {
  @Input() value = '';
  @Input() otherValue = '';

  @Output() valueChanged = new EventEmitter<string>();
  @Output() otherValueChanged = new EventEmitter<string>();

  @ViewChild(ChildDirective) child?: ChildDirective;

  @ViewChildren(ChildDirective) viewChildren?: QueryList<ChildDirective>;

  @ContentChild(ChildDirective) contentChild?: ChildDirective;

  @ContentChildren(ChildDirective) contentChildren?: QueryList<ChildDirective>;

  private hiddenValue = '';

  constructor() {}
  
  ngOnInit(): void {}

  @HostBinding('class.valid')
  isValid(): boolean {
    return true;
  }

  @HostListener('click', ['$event'])
  onClick(event): void {}

  myPublicFunc(): void {}
  
  private myPrivateFunc(): void {}
}

Back to top

Lifecycle Hooks Interfaces and Order

Adding lifecycle hooks interfaces is not mandatory but a suggested practice.

Ideally, they should be defined in the same order they execute and after the constructor.

class MyComponent implements OnChanges, OnInit, DoCheck,
  AfterContentInit, AfterContentChecked, AfterViewInit,
  AfterViewChecked, OnDestroy {

  constructor() {}

  ngOnChanges(): void {}

  ngOnInit(): void {}

  ngDoCheck(): void {}

  ngAfterContentInit(): void {}

  ngAfterContentChecked(): void {}

  ngAfterViewInit(): void {}

  ngAfterViewChecked(): void {}

  ngOnDestroy(): void {}	
}

Back to top

Write Logic Outside Lifecycle Hook

Avoid directly writing any logic within the lifecycle hooks, encapsulate logic within private methods and call them within the lifecycle hooks.

export class MyComponent implements OnInit {
  ngOnInit(): void {
    this.init();
  }
  
  private init(): void {
   // ...
  }
}

Back to top

Component Event Names Rules

Follow this rules will help you to decide better event names:

  • Do not prefix events/outputs names with on. The handler, instead, could be written with such prefix
  • Always specify the entity whose action referred, not only the action itself. If we are describing an event on a component whose value changed, the event change could be valueChange.
  • Use past-sense or not (valueChange or valueChanged)

Back to top

HTML Wrapping and Order

Do not exceed 80 characters per column for all files, it’s simply much easier to read vertically than horizontally.

Rules for Writing HTML Tags

  • When an element has two or more attributes, write one attribute per line
  • Attributes have to be written in a specific order
  • Unless using a single attribute, the closing tag has to be written on the next line
  • Add structural directives only to ng-container elements

Attributes order:

  1. Structural directives (*ngIf, *ngFor, *ngSwitch)
  2. Animations (@myAnimation)
  3. Template variables (#myElement)
  4. Static properties (id, class, aria-label)
  5. Dynamic properties ([id], [class], [attr.aria-label])
  6. Events ((click), (myEvent))
  7. Two-way binding ([(value)])

Why?: This can facilitate reading through and understanding the structure of your templates.

Before

<input #myElement (input)="onInputChanged($event)" [(value)]="myModel" *ngIf="canEdit" class="form-control" [attr.placeholder]="placeholder" @fadeIn type="text" />

After

<ng-container *ngIf="canEdit"> 
  <input
    @fadeIn
    #myElement
    type="text"
    class="form-control"
    [attr.placeholder]="placeholder"
    (input)="onInputChanged($event)"
    [(value)]="myModel"
  />
</ng-container>

Back to top

Wrap Pipes Within Parenthesis

Wrap pipes expressions within parenthesis.

Why?: The feeling of division provided by the parenthesis gives a clue that the value is being transformed.

<ng-container *ngIf="(movies$ | async) as movies">
  <!-- ... -->
</ng-container>

When using multiple pipes, it may even be more important:

<input 
 [value]="(value$ | async | uppercase | trim)"
/>

Back to top

Avoid Logic in Templates

If you have any sort of logic in your templates, even if it is a simple && clause, it is good to extract it out into its component.

Why?: Having logic in the template means that it is not possible to unit test it, and therefore it is more prone to bugs when changing template code.

Before

<p *ngIf="role === 'developer'">Status: Developer</p>
@Input() role?: Role;

After

<p *ngIf="isDeveloper"></p>
isDeveloper: boolean;

@Input()
set role(role: Role) {
  this.isDeveloper = role === 'developer';
}

Back to top

Prevent Memory Leaks

When we navigate from one component to the other component, the first component is destroyed and the other component initializes. The first component was subscribed to the Observable and now the component is destroyed. This can cause memory leaks.

Use of takeUntil Operator

takeUntil is also an operator. It allows monitoring the Observables and get rid of the subscriptions once the value is emitted by Observables. We can conclude that it secures the Observables from getting leaked.

ngUnsubscribe$ = new Subject<void>();

ngOnInit(): void {
  this.movieService.getListUpdates()
    .pipe(takeUntil(this.ngUnsubscribe$))
    .subscribe(movies => {
      this.movies = movies;
    });
}

ngOnDestroy(): void {
  this.ngUnsubscribe$.next();
}

Use of async Pipe

It subscribes to an Observable or Promise and returns to the recent emitted value and unsubscribe when the component is destroyed.

<ul *ngIf="(movieService.getListUpdates() | async) as movies">
  <li *ngFor="let movie of movies">
    {{ movie.title }}
  </li>
</ul>

Use of take(1)

It takes the value and allows for not subscribing whenever a new value is diagnosed. It takes care that you receive data only once.

this.movieService.getList()
  .pipe(take(1))
  .subscribe(movies => {
    this.movies = movies;
  });

Back to top

Subscribe in Template Using async Pipe

Avoid subscribing to observables from components and instead subscribe to the observables from the template.

Why?: async pipe unsubscribe automatically, and it makes the code simpler by eliminating the need to manually manage subscriptions. It also reduces the risk of accidentally forgetting to unsubscribe a subscription in the component, which would cause a memory leak. This risk can also be mitigated by using a lint rule to detect unsubscribed observables.

Before

<p>{{ textToDisplay }}</p>
textToDisplay = '';

ngOnInit(): void {
  this.textSubscriotion = this.textService
    .pipe(
      map(value => value.item),
    )
    .subscribe(item => this.textToDisplay = item);
}

ngOnDestroy(): void {
  if (this.textSubscriotion) {
    this.textSubscriotion.unsubscribe();
  }
}

After

<p>{{ (textToDisplay$ | async) }}</p>
textToDisplay$ = this.textService.pipe(map(value => value.item));

Back to top

Use Change Detection OnPush

Use the OnPush change detection strategy to tell Angular there have been no changes. This lets you skip the entire change detection step. This change detection works by detecting if some new data has been explicitly pushed into the component, either via a component input or an Observable subscribed to using the async pipe.

Why?: The more use of OnPush in components we have the fewer checks Angular needs to perform, means better performance.

<app-todo *ngFor="let todo of todos" [todo]="todo"></app-todo>
@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoListComponent {
  @Input() todos: Todo[] = [];
}

Back to top

Avoid Having Subscriptions Inside Subscriptions

Sometimes you may want values from more than one observable to perform an action. In this case, avoid subscribing to one observable in to subscribe block of another observable. Instead, use appropriate chaining operators. Chaining operators run on observables from the operator before them. Some chaining operators are: withLatestFrom, combineLatest, etc.

Before

firstObservable$.pipe(
    take(1)
  )
  .subscribe(firstValue => {
    secondObservable$.pipe(
        take(1)
      )
      .subscribe(secondValue => {
        console.log(`Combined values are: ${firstValue} & ${secondValue}`);
      });
  });

After

firstObservable$.pipe(
    withLatestFrom(secondObservable$),
    first()
  )
  .subscribe(([firstValue, secondValue]) => {
      console.log(`Combined values are: ${firstValue} & ${secondValue}`);
  });

Back to top

Use trackBy Along With ngFor

When using ngFor to loop over an array in templates, use it with a trackBy function which will return a unique identifier for each item.

Why?: When an array changes, Angular re-renders the whole DOM tree. But if you use trackBy, Angular will know which element has changed and will only make DOM changes for that particular element.

Before

<li *ngFor="let movie of movies">{{ movie.title }}</li>

After

<li *ngFor="let movie of movies; trackBy: trackByFn">{{ movie.title }}</li>
trackByFn(index, movie: Movie): string {
  return movie.id;
}

Back to top

Strings Should Be Safe

If you have a variable of type string that can have only a set of values, instead of declaring it as a string type, you can declare the list of possible values as the type.

Why?: By declaring the type of the variable appropriately, we can avoid bugs while writing the code during compile time rather than during runtime.

export class ButtonComponent {
  @Input() type: string;
}
export class ButtonComponent {
  @Input() type: 'submit' | 'reset' | 'button' = 'button';
}

Back to top

Avoid any Type

Declare variables or constants with proper types other than any.

Wny?: This will reduce unintended problems. Another advantage of having good typings in our application is that it makes refactoring easier.

Before

export class MovieComponent {
  movie: any;
  
  constructor() {
    this.movie = {
      // Whatever we want can be decaler here...
    };
  }
}

After

interface Movie {
  title: string;
}

export class MovieComponent {
  movie: Movie;

  constructor() {
    this.movie = {
      title: 'Avengers',
      // We can't decalre other properties...
    };
  }
}

Back to top

Use Mandatory Inputs

To make the requirement explicit we can use the selector in the @Component decorator to require that the attribute on our component must exist.

@Component({
  selector: 'movie-list[movies]',
})
export class MovieListComponent {
  @Input() movies: Movies[];
}

Resulting an error, when we start the application or at compile time when the application is built Ahead of Time (AoT), if the MovieListComponent doesn't have a movies attribute. This approach improves the readability of the code because helps other developers to integrate this component into their projects, throwing errors, and we don't need to define explicit validations to check if it exists.

Back to top

Do Not Pass Streams to Components Directly

Passing streams to child components is a bad practice because it creates a very close link between the parent component and the child component. A component should always receive an object or value and should not even care if that object or value comes from a stream or not. It is better to handle the subscription in the parent component itself. Angular has a feature called the async pipe that can be used for this.

Back to top

Do Not Pass Streams to Services

By passing a stream to a service we don't know what's going to happen to it. The stream could be subscribed to, or even combined with another stream that has a longer lifecycle, that could eventually determine the state of our application. Subscriptions might trigger unwanted behavior. It's recommended to use higher order streams in the components for these situations.

Back to top

Do Not Expose Subjects

In order to avoid side effects to subject value it's better to hide the subject itself in the service and to expose the observable of the subject and function update his values.

export class CartService {
  private selectedItem: BehaviorSubject<CartItem | null> = new BehaviorSubject<CartItem | null>(null);

  readonly selectedItem$: Observable<CartItem | null> = this.selectedItem.asObservable();

  updateSelectedItem(item: CartItem): void {
    this.selectedItem.next(item);
  }
}

Back to top

Handle RxJS Errors

Error handling is an essential part of RxJS. By default, if something goes wrong with an Observable, it just dies. If we don't deal with such errors, it will happen silently, and we won't know why we are not receiving data anymore.

Back to top

Avoid Changing the DOM Directly

It's important to know that Angular uses Lifecycle Hooks that determine how and when components will be rendered and updated. Direct DOM access or manipulation can corrupt these lifecycle hooks, leading to unexpected behavior of the whole app. Direct access to the DOM can make our application more vulnerable to XSS attacks. Use ElementRef as the last resort when direct access to DOM is needed. Use templating and data-binding provided by Angular instead. Alternatively we can take a look at Renderer2 which provides API that can safely be used even when direct access to native elements is not supported.

Back to top

Avoid Computing Values in the Template

Sometimes in Angular templates, we may be tempted to bind a method in the HTML template. The problem is that the methods are constantly getting called during the Angular Change Detection Cycle.

<h1>{{ getTitle() }}</h1>
export class HomeComponent {
  getTitle(): string {
    return 'Home Page';
  }
}

It's highly recommended not to use methods calls in Angular template expressions. While method calls in Angular templates are super convenient and technically valid, they may cause serious performance issues because the method is called every time change detection runs. Instead, we can use pure pipes or manually calculate the values with Lifecycle Hooks.

Back to top

Use Immutability

Objects and arrays are the reference types in javascript. If we want to copy them into another object or an array and to modify them, the best practice is to do that in an immutable way using spread operator … this will prevent from changing the original object or array.

In Angular, it's very critical since we can modify the original array or object in the service or component and get unexpected behavior.

// Somewhere in the code we have list of movies...
const movies = [
  {
    title: 'Avengers',
    year: 2012
  },
];

// And in other place we get the movies list...
const updatedMovies = [...movies]; // Update with spread operator...

Please notice that using spread operator is copy only one level! you need to use spread operator to each level or instead to use deep cloning like this:

const updatedMovies = JSON.parse(JSON.stringify(movies));

Back to top

Safe Navigation Operator in HTML Template

To be on the safe side we should use the safe navigation operator while accessing a property from an object in a component’s template. If the object is null, and we try to access a property, we are going to get an exception. But if we use the save navigation (?) operator, the template will ignore the null value and will access the property once the object is not the null anymore.

<ng-container *ngif="movie">
  <p>{{ movie.details?.description }}</p>
</ng-container>

Back to top

Sanitize Untrusted Values

Unless you enforce Trusted Types, the built-in browser DOM APIs don't automatically protect you from security vulnerabilities. For example, document, the node available through ElementRef, and many third-party APIs contain unsafe methods. In the same way, if you interact with other libraries that manipulate the DOM, you likely won't have the same automatic sanitization as with Angular interpolations. Avoid directly interacting with the DOM and instead use Angular templates where possible.

For cases where this is unavoidable, use DomSanitizer.sanitize method and the appropriate SecurityContext to sanitize untrusted values.

export class SomeComponent {
  content = this.domSanitizer.sanitize(
    SecurityContext.HTML,
    `<img src="" alt="" onerror="alert('You have been attacked')" />` // This data coule be come from outside API
  );

  constructor(private domSanitizer: DomSanitizer) {}
}

Back to top

Use InjectionToken for Dependency Injection

InjectionToken is both unique and symbolic, a JavaScript object that has a friendly name but won't conflict with another token that happens to have the same name.

Use InjectionToken in case you are not using classes as dependency injection.

Why: When using string instead of InjectionToken it can be led to conflicts.

InjectionToken variable name should be written as UPPER_CASE with TOKEN prefix.

Why: It will allow the developer to recognize it's InjectionToken which can be provided.

Before

@NgModule({
  providers: [
    {
      provide: 'TITLE',
      useValue: 'My Site',
    },
  ],
})
export class SomeModule {}

After

import { InjectionToken } from '@angular/core';

export const TITLE_TOKEN = new InjectionToken<string>('title');
@NgModule({
  providers: [
    {
      provide: TITLE_TOKEN,
      useValue: 'My Site',
    },
  ],
})
export class SomeModule {}
@Injectable({
  provided: 'root',
})
export class SomeService {
  constructor(@Inject(TITLE_TOKEN) private readonly title: string) {}
}

Back to top

Prevent Importing Module in Feature Modules

You can easily prevent from importing the module in feature modules and to enforce it to be used only in the AppModule (the root module) by checking who trying to create the class.

Why: It helps by accidentally providing the module on unappropriated modules and providing dependencies with configuration easier.

Please notice I provide the AuthService in the forRoot static method because I want to make sure the developer who want to use this service provide me also the configuration object.

import {
  ModuleWithProviders,
  NgModule,
  Optional,
  SkipSelf,
} from '@angular/core';

import {
  AUTH_CONFIG_TOKEN,
  AuthConfig,
  authConfigDefault,
  AuthService,
} from './auth';

@NgModule()
export class AuthModule {
  constructor(@Optional() @SkipSelf() parentModule?: AuthModule) {
    if (parentModule) {
      throw new Error(
        'AuthModule is already loaded. Import it in the AppModule only'
      );
    }
  }

  static forRoot(
    config: Partial<AuthConfig> = {}
  ): ModuleWithProviders<AuthModule> {
    return {
      ngModule: AuthModule,
      providers: [
        AuthService,
        {
          provide: AUTH_CONFIG_TOKEN,
          useValue: {
            ...authConfigDefault,
            ...config,
          },
        },
      ],
    };
  }
}

Back to top

Break Down Into Small Reusable Components

This might be an extension of the Single responsibility principle. Large components are very difficult to debug, manage and test. If the component becomes large, break it down into more reusable smaller components to reduce duplication of the code, so that we can easily manage, maintain and debug with less effort.

--ParentComponent
----TitleComponent
----BodyComponent
----ListComponent
------ItemComponent
----FooterComponent

Back to top

Use Smart and Dumb Components

This pattern helps to use OnPush change detection strategy to tell Angular there have been no changes in the dumb components.

Smart components are used in manipulating data, calling the APIs, focussing more on functionalities, and managing states. While dumb components are all about cosmetics, they focus more on how they look.

Back to top

Use Lazy Loading

When possible, try to lazy load the modules in your Angular application. Lazy loading is when you load something only when it is used, for example, loading a component only when it is to be seen.

Why?: This will reduce the size of the application to be loaded and can improve the application boot time by not loading the modules that are not used.

Before

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    component: HomeComponent,
  },
];

After

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    loadChildren: () => import('./features/home/home.module').then(m => m.HomeModule),
  },
];

Back to top

Use index.ts

Instead of remembering multiple source file names, there are some tiny import statements that will fulfill the purpose.

Why?: It helps to keep all correlated files in a single location.

Before

import { uuid } from './../utils/uuid';
import { convertToTitleCase } from './../utils/convert-to-title-case';

After

utils/index.ts

export * from './uuid';
export * from './convert-to-title-case';

Now we can import all the files from one file.

import { uuid, convertToTitleCase } from './../utils';

Back to top

Isolate API Hacks

Sometimes, we need to add some logic in the code to make up for bugs in the APIs. Instead of having the hacks in components where they are needed, it is better to isolate them in one place like in a function or a service and use them. When fixing the bugs in the APIs, it is easier to look for them in one file rather than looking for the hacks that could be spread across the codebase.

Back to top

Cache API Calls

Caching API calls improves performance and saves memory by limiting server requests to fetch redundant information. Some API responses may not change frequently so we can put some caching mechanism and can store those values from the API. When the same API request is made, we can get a response from the cache. Thus, it speeds up the application and also makes sure that we are not downloading the same information frequently.

You can use for example RxJS shareReplay operator.

class MoviesService {
  private commonMovies$: Observable<Movie[]> | null = null;

  
  getCommonMovies(): Observable<Movie[]> {
    if (!this.commonMovies$) {
      this.commonMovies$ = this.httpClient.get<Movie[]>('/api/movies/common').pipe(
        shareReplay(1),
      );
    }

    return this.commonMovies$;
  }
}

Back to top

Provide Private Services

Most providers in angular are designed to act on a global scope. They are then provided at an application level (AppModule). This makes sense if the use of the global-singleton-pattern is required. However, there are services that do not need to be provided globally. Especially if they are used by just one component. In that case, it can make sense to provide that service inside the component, instead of globally. That is, if the service is directly tied to that component, as shown below.

@Component({
  selector: 'app-navbar',
  providers: [NavbarService],
})
export class NavbarComponent {}

Providers are tree-shakable, the Angular compiler removes the associated services from the final output when it determines that our application doesn't use those services. It also minimizes the risk of dead code and reduces the size of our bundles.

Back to top

Avoid Risky Angular APIs

Avoid Angular APIs marked in the documentation as Security Risk. The most common risky API we use is ElementRef. It permits direct access to the DOM and can make your application more vulnerable to XSS attacks. Review any use of ElementRef in your code carefully. Use this API as a last resort when direct access to the DOM is needed. Use templating and data binding provided by Angular, instead. Alternatively, you can take a look at Renderer2, which provides an API that can safely be used even when direct access to native elements is not supported.

Back to top

Avoid Poorly Structured CSS

Common mistakes are excessive use of deep selectors and inline styles. Inline styles are considered as bad practice due to poor scalability and maintainability. As a rule of thumb, define all styles in the CSS files. Usage of ::ng-deep to overwrite styles in other components is incredibly popular. Despite being a working solution, it's marked as deprecated. The main reason for that is that this mechanism for piercing the style isolation sandbox around a component can potentially encourage bad styling practices. Though, it isn't going away until Angular implements ::part() and ::theme() from the CSS Shadow Parts spec, as there is no better alternative.

Back to top

Lack of Meaningful Unit Tests

Angular CLI encourages writing unit tests by spanning out *.spec.ts files with every created component. However, don't leave them empty or be satisfied by configuring the TestBed with component initialization without actual tests. If developers don't write tests, then absence of a test file would clearly indicate the state of affairs to other developers, rather than misleading them by giving a false sense of security with a rudiment *.spec.ts file. We need to cover with tests the most fragile parts, rather than covering what's easier to test.

Back to top

Avoid Useless Code Comments

Comments are considered a best practice, but if we are adding a comment, it's because it's not self-explanatory, and we should choose a better way to implement it.

Good comments are informative comments, when be useful to provide basic information. For example, a comment that contains legal information, or are a warning, when we are working with multiple developers on a project, we could use a comment to warn other developers about certain consequences, or are a to-do comments for tasks a developer thinks should be done, but for some reason can't be done at this moment.

Bad Comments are commented-out code is a common practice, but we shouldn't do it, because other developers will think the code is there for a reason and won't have the courage to delete it. Just delete the code. We have got version control, so the code isn't lost forever. Another case is noise comments. Some comments that we see are just noise. They restate the obvious and serve no real purpose. Redundant comments are comments that are not more informative than the code. These comments only clutter the code.

Back to top

Remove Unused Code

Unused code or dead code is any code which will never be executed. It may be some condition, loop or any file which was simply created but wasn't used in our project. It is a problem because that code has no sense, and we can drop it. Dead-code Elimination also reduces the size of our bundles and repositories. Less code also increases maintenance, IDE performance and makes it easier to understand. Common mistakes in TypeScript projects are unused imports, variables, functions and private class members.

Back to top

Avoid Using Third-Party Libraries

Usage of third-party libraries should be avoided and should be as final destination.

If you decided to use third-party library try to explain to the team why it was chosen and why it can not be done with Angular built-in libraries, it will make good conversation with the team and maybe could lead to better solutions.

Why: Angular comes with a great set of built-in libraries such as routing, forms, HTTP Client, testing and many more. When deciding to add third-party library the bundle size will become bigger and that resulting to slower performance.

Back to top

Base Component Classes

Create a base class component may come in handy when we have lots of reused stuff and don't want to pollute each component with the same code all over. Common situations are when we are creating form components, when we have pages with the same behavior, such as pages with HTML forms. In these examples, having the same code in multiple places means that if we want to make a change to the logic in that code, we must do it in multiple places. We can create a base class with the common data and methods. Thus, we don't have the same duplicate code in different locations in the code base.

Back to top

Do Not Remove View Encapsulation

In Angular, component CSS styles are encapsulated into the component's view and don't affect the rest of the application. To control how this encapsulation happens on a per component basis, we can set the view encapsulation mode in the component metadata. The default is Emulated, and it emulates the behavior of Shadow DOM by preprocessing the CSS code to effectively scope the CSS to the component's view. In the None mode, styles from the component propagate back to the main HTML and therefore are visible to all components on the page. We can use this option, but we need to be careful and adopt other strategies like nested CSS or naming conventions.

Back to top

Analyze the Bundle Size

There is a great tool webpack-bundle-analyzer that can help us to identify how much size every library consume in the final bundle.

Why: Knowing what library or import impact on the final bundle can help us to shrink the size of the bundle by maybe removing the library or replace it with lighter one.

The usage is very easy:

npm i -D webpack-bundle-analyzer
ng build --stats-json # Build angular with stats JSON file
webpack-bundle-analyzer dist/my-app/stats.json

Back to top

Use CSP To Prevent XSS

CSP (Content Security Policy) help us to prevent XSS attacks very easily by restricting what resources can be used in the website.

How to use CSP

Back to top

Use ECMAScript Features

ECMAScript is one of the JavaScript versions which constantly get updated with new features and functionalities.

Because Angular using the TypeScript language, TypeScript is always updated according to the new ECMAScript features, so you can utilize them in Angular.

TypeScript Updates

Back to top

Use Reactive Forms

Angular presents two different methods for creating forms: template-driven and reactive forms. Reactive forms provide a model-driven approach to handling form inputs whose values change over time. The Reactive approach removes validation logic from the template, keeping the templates clean of validation logic. Reactive forms use an explicit and immutable approach to managing the state of a form at a given point in time. Each change to the form state returns a new state, which maintains the integrity of the model between changes.

Back to top

Use CDK Virtual Scroll

CDK (Component Development Kit) Virtual Scroll can be used to display large lists of elements more efficiently. Virtual scrolling enables an efficient way to simulate all values by making the height of the container element equal to the height of the total number of elements.

For more info: Angular material cdk virtual scroll

Back to top

Use Angular Service Workers and PWA

Angular service worker is designed to optimize the end user experience of using an application over a slow or unreliable network connection, while also minimizing the risks of serving outdated content.

For more info: How to use Angular service worker and pwa

Back to top

Use Angular Universal

A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions. Angular Universal executes on the server, generating static application pages that later get bootstrapped on the client. This means that the application generally renders more quickly, giving users a chance to view the application layout before it becomes fully interactive.

For more info: Server-side rendering (SSR) with Angular Universal

Back to top

Use Lint Rules

ESLint and Stylelint have various inbuilt options, it forces the program to be cleaner and more consistent. It is widely supported across all modern editors and can be customized with your own lint rules and configurations. This will ensure consistency and readability of the code.

Back to top

Use Angular DevTools Chrome Extension

Angular DevTools extends Chrome DevTools adding Angular specific debugging and profiling capabilities.

  • Understand the structure of your application
  • Preview the state of the directive and the component instances
  • See change detection cycles, what triggered them, and how much time Angular spent in them

Angular DevTools Chrome Extension

Angular DevTools Overview

Back to top

Use Storybook

Storybook is an open source tool for building UI components and pages in isolation. It streamlines UI development, testing, and documentation.

Why?: By using storybook it enforces us to make better isolated components, easier to search for components that can be used for new developers in the team, great for product team to view the new UI and good documentation for the company UI design.

Storybook

Back to top

License

MIT

About

Angular guide for teams that look for consistency through best practices

Topics

Resources

Stars

Watchers

Forks