DEV Community

Sonu Kapoor for This is Angular

Posted on

Optimizing Angular Reactive Forms: Enhancing Performance with Lazy Validation and Async Validators

Angular’s Reactive Forms provide a powerful way to handle forms in web applications, giving developers fine-grained control over the structure and behaviour of form inputs, validations, and events. However, as forms become more complex, performance can suffer, especially in large-scale applications with multiple nested form groups and heavy validation logic. This is where performance optimization strategies, like lazy validation and async validators, become crucial.

In this article, we’ll dive into several advanced techniques for optimizing Angular Reactive Forms, focusing on performance improvements through lazy validation, async validators, and other form-handling strategies that help keep your applications responsive and efficient.


Why Optimize Angular Reactive Forms?

Before we dive into the techniques, it’s essential to understand why optimization is necessary. Forms are an integral part of most web applications, and as the complexity of the form increases, issues such as slow responsiveness, inefficient validation checks, and user frustration can arise.

Key reasons for optimizing Angular forms include:

  • Improving user experience: By reducing unnecessary re-renders and cutting down on inefficient validations, users get a smoother and faster experience.
  • Reducing validation overhead: Ensuring that validation logic is executed only when needed can save significant computation time.
  • Handling complex scenarios: When dealing with large forms, nested form groups, or dynamic form controls, performance can degrade without careful optimization.

Lazy Validation: Validate When It’s Necessary

What is Lazy Validation?

By default, Angular Reactive Forms trigger validation on every value change. While this works for simple forms, in more complex scenarios where forms have nested controls and custom validators, validating every time a value changes can significantly degrade performance.

Lazy validation allows us to delay validation until it is actually necessary, reducing the frequency of validation checks and improving overall form performance.

Setting Up Lazy Validation

By default, form controls are validated as soon as a value changes. To optimize this behavior, we can configure the form control to validate only under specific circumstances:

  1. On Blur Validation: This approach triggers validation only when the user finishes interacting with a form control (e.g., when they leave a field).

  2. Manual Validation: Validation can be triggered manually, allowing you to validate an entire form or a subset of form controls only when necessary (e.g., on form submission).

Let’s look at an example to demonstrate both approaches.

Example: Lazy Validation with On Blur Trigger

import { Component } from "@angular/core";
import { FormControl, Validators } from "@angular/forms";

@Component({
  selector: "app-lazy-validation-form",
  template: `
    <form>
      <label for="username">Username:</label>
      <input id="username" [formControl]="usernameControl" />
      <div *ngIf="usernameControl.invalid && usernameControl.touched">
        Username is required and must be at least 3 characters long.
      </div>
    </form>
  `,
})
export class LazyValidationFormComponent {
  usernameControl = new FormControl("", {
    validators: [Validators.required, Validators.minLength(3)],
    updateOn: "blur",
  });
}
Enter fullscreen mode Exit fullscreen mode

In this example, we’re using the updateOn: 'blur' configuration, which means validation will only run after the user moves away from the username field (i.e., when the field loses focus). This prevents the form control from triggering validation after every keystroke.

Example: Manual Validation on Form Submission

import { Component } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";

@Component({
  selector: "app-manual-validation-form",
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <label for="email">Email:</label>
      <input id="email" formControlName="email" />
      <div *ngIf="form.controls.email.invalid && form.controls.email.touched">
        Please enter a valid email.
      </div>

      <label for="password">Password:</label>
      <input id="password" type="password" formControlName="password" />
      <div
        *ngIf="form.controls.password.invalid && form.controls.password.touched"
      >
        Password is required and must be at least 6 characters.
      </div>

      <button type="submit">Submit</button>
    </form>
  `,
})
export class ManualValidationFormComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      email: ["", [Validators.required, Validators.email]],
      password: ["", [Validators.required, Validators.minLength(6)]],
    });
  }

  onSubmit() {
    if (this.form.invalid) {
      // Manually mark all controls as touched to trigger validation
      this.form.markAllAsTouched();
      return;
    }
    // Form is valid, proceed with submission
    console.log("Form submitted", this.form.value);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, validation is triggered manually when the form is submitted. If the form is invalid, we mark all form controls as "touched" so validation messages will appear. This approach ensures validation is performed only when necessary, reducing unnecessary overhead while the user is filling out the form.


Async Validators: Dealing with External Data Sources

Async validators are particularly useful when you need to validate form input against an external data source, such as checking if a username or email is already taken by querying a backend API. Unlike synchronous validators, async validators can return a promise or an observable, allowing you to handle asynchronous operations within your form validation logic.

Why Use Async Validators?

  • Efficient Validation: Instead of running heavy synchronous logic, async validators allow you to perform checks asynchronously without blocking the main UI thread.
  • API Integration: Validate form data by interacting with backend systems, such as checking if a username or email address is unique.

Implementing an Async Validator

Let’s look at an example where we implement an async validator to check if a username is already taken by querying a backend service.

Example: Async Validator for Username Availability

import { Injectable } from "@angular/core";
import { AbstractControl, AsyncValidatorFn } from "@angular/forms";
import { Observable, of } from "rxjs";
import { catchError, debounceTime, map, switchMap } from "rxjs/operators";
import { HttpClient } from "@angular/common/http";

@Injectable({ providedIn: "root" })
export class UsernameValidator {
  constructor(private http: HttpClient) {}

  validateUsername(): AsyncValidatorFn {
    return (
      control: AbstractControl
    ): Observable<{ [key: string]: any } | null> => {
      return control.valueChanges.pipe(
        debounceTime(300),
        switchMap((username) => this.checkUsername(username)),
        map((isTaken) => (isTaken ? { usernameTaken: true } : null)),
        catchError(() => of(null))
      );
    };
  }

  private checkUsername(username: string): Observable<boolean> {
    // Simulating an API call to check if the username is taken
    return this.http.get<boolean>(`/api/check-username?username=${username}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we use RxJS operators like debounceTime and switchMap to prevent unnecessary API calls and ensure that we only query the backend after the user has finished typing. The async validator returns a validation error (usernameTaken: true) if the username is already taken, and null if the username is available.

Usage in a Form Control

import { Component } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { UsernameValidator } from "./username-validator.service";

@Component({
  selector: "app-async-validator-form",
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <label for="username">Username:</label>
      <input id="username" formControlName="username" />
      <div
        *ngIf="form.controls.username.invalid && form.controls.username.touched"
      >
        <div *ngIf="form.controls.username.errors?.usernameTaken">
          Username is already taken.
        </div>
        <div *ngIf="form.controls.username.errors?.required">
          Username is required.
        </div>
      </div>

      <button type="submit">Submit</button>
    </form>
  `,
})
export class AsyncValidatorFormComponent {
  form: FormGroup;

  constructor(
    private fb: FormBuilder,
    private usernameValidator: UsernameValidator
  ) {
    this.form = this.fb.group({
      username: [
        "",
        [Validators.required],
        [this.usernameValidator.validateUsername()],
      ],
    });
  }

  onSubmit() {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
    console.log("Form submitted", this.form.value);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we use the async validator to check if the username is already taken. The form control username uses both a synchronous validator (Validators.required) and an async validator (this.usernameValidator.validateUsername()). When the user submits the form, the async validator ensures that the form cannot be submitted if the username is already in use.


Optimizing Form Rendering Performance

Beyond validation, optimizing how Angular Reactive Forms render can make a significant difference in application performance. By default, Angular's change detection mechanism runs checks on every component and input event, which can become a performance bottleneck in large applications.

Here are a few strategies to optimize form rendering performance:

  1. OnPush Change Detection Strategy

By default, Angular uses the Default change detection strategy, which checks all components for changes. You can optimize performance by using the OnPush strategy, which limits change detection to situations where a component’s input properties change or an event occurs within the component.

Example:

@Component({
  selector: "app-optimized-form",
  templateUrl: "./optimized-form.component.html",
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedFormComponent {
 // Component logic
}
Enter fullscreen mode Exit fullscreen mode
  1. TrackBy for ngFor Loops

When rendering lists of form controls (e.g., dynamic forms or repeated input fields), using trackBy in ngFor can prevent Angular from re-rendering the entire list when only a specific item changes.

Example:

<div *ngFor="let control of formArray.controls; trackBy: trackByIndex">
   <input [formControl]="control" />
</div>
Enter fullscreen mode Exit fullscreen mode
trackByIndex(index: number): number {
   return index;
}
Enter fullscreen mode Exit fullscreen mode
  1. Lazy Load Modules with Forms

If your application contains complex forms spread across different routes, consider lazy-loading the modules containing these forms. This reduces the initial load time of your application and ensures that forms are only loaded when needed.

Example:

const routes: Routes = [{
  path: "user-form",
  loadChildren: () =>
     import("./user-form/user-form.module").then((m) => m.UserFormModule),
  },
];
Enter fullscreen mode Exit fullscreen mode

Conclusion

Optimizing Angular Reactive Forms is crucial for creating efficient, responsive, and scalable web applications. By implementing lazy validation, async validators, and rendering optimizations, you can significantly improve the performance of your forms, especially in complex scenarios with nested controls and external data validation.

With these strategies in place, your forms will be not only faster but also more maintainable and easier to manage, ensuring a better experience for both developers and users.

If you’re working with large or dynamic forms, take the time to implement these techniques, and you’ll see noticeable improvements in both performance and user satisfaction.

Top comments (2)

Collapse
 
morev1993 profile image
Igor • Edited

Hi, Sonu. Thanks for your article. I also would like to hear about properly using FormControl methods flags like onlySelf, emitEvent. I think it can reduce some valueChange event calls.

Collapse
 
hakimio profile image
Tomas Rimkus
  • In the async validator example, If (this.form.invalid) { ... } will give you incorrect results if onSubmit() is called before your async validator has finished validation.
  • You are using some outdated Angular concepts. *ngFor has been replaced by the built-in @for control flow block where it's easier to supply the track by property. Constructor DI is also no longer used in favor for inject() function. Components should be standalone (which will be the default in Angular 19).