Don't use Signals with Angular Reactive Forms

4 min read


Hey folks! In a recent post about Angular signals and forms, I mentioned some potential issues when using Angular signals with the Reactive Forms API. Today, let's take a deep dive to fully understand this gray area and find some good solutions.

Video

Getting on the Same Page

First, let's revisit our example from the previous post on Template Driven Forms. The idea is two input fields - 'first name' and 'last name', both needing validation. We also have a computed 'full name' signal combining the inputs.

Finally, we want to update the signal value and form value with a button click.

Initial Forms Setup

Bringing in Reactive Forms

Let's start by importing the Reactive Forms module in the standalone app component.

import { ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    ...
    ReactiveFormsModule,
  ],
})

Next, we'll set up the form controls for first and last name with validators, because that is how we need to do it with reactive forms.

firstNameControl = new FormControl("", Validators.required);
lastNameControl = new FormControl("", Validators.required);

With the controls defined, we need to connect them to the inputs using the FormControl directive:

<mat-form-field>
  <input matInput placeholder="First Name" [formControl]="firstNameControl" />
</mat-form-field>
<mat-form-field>
  <input matInput placeholder="Last Name" [formControl]="lastNameControl" />
</mat-form-field>

Then we can add the error messages like with Template Driven Forms:

<mat-error *ngIf="firstNameControl.hasError('required')">
  First name is required
</mat-error>

So far, so good! The controls work as expected including their validations!

Form Control Validations work

Checking Out One-Way Binding

Let's look at how we can convert our form control values into signals. Angular gives us the valueChanges observable we can turn into a signal with toSignal() to track control value changes. toSignal() can be imported from the rxjs-interop package.

import { toSignal } from "@angular/core/rxjs-interop";

firstName = toSignal(this.firstNameControl.valueChanges);
lastName = toSignal(this.lastNameControl.valueChanges);

But this seems to only work one way. It updates the signal when the control changes. What if we want to update the control value from a signal change on a button click?

Implementing Two-Way Data Binding

But wait, when we try to do that to our firstName signal, Angular throws an error saying "set was not found on firstName".

That is because toSignal only creates a read only signal.

So how do we create a signal which tracks the form control values and also allows us to set its value from a button or another external operation?

We can simply subscribe to the valueChanges observable and use a writeable signal to set the values explicitly.

firstName = signal('');
lastName = signal('');

constructor() {
    this.firstNameControl.valueChanges.subscribe((val) =>
      this.firstName.set(val ?? '')
    );
    this.lastNameControl.valueChanges.subscribe((val) =>
      this.lastName.set(val ?? '')
    );
}

Ok, so now we can update values of our signals as well by adding a button handler like below.

<button mat-raised-button (click)="firstName.set('Zoaib')">
  Set First Name to 'Zoaib'
</button>

Great!

But how do we update the form control values whenever the signals are updated from a button or elsewhere?

We can create an effect in the constructor to call setValue() on the control when the signal changes:

constructor() {
  effect(() => {
    this.firstNameControl.setValue(this.firstName());
    this.lastNameControl.setValue(this.lastName());
  });
}

Reminder: An effect has to be setup in an injection context, so it has to be in a constructor or a lifecycle method. More about Angular effects here.

Now when we click on the button and update the firstName signal, the form control updates automatically - because the effect will be executed! Also, in turn our computed will be also be updated and show the first name! Cool :)

Two-way data binding works

Now we have two-way binding! But is this worth it for Reactive Forms?

Duplication and Messy Code

Looking closer, even though the template is simpler than template-driven forms, we have duplication with the control values and the writable signals. Both contain the same form control value at any time.

We also have to manually subscribe to the valueChanges whenever the form control changes to update the signal value. Not very nice!

This gets crazy with large forms!

Maybe FormGroup could help because it would group controls in one signal object, but managing the code still seems hard.

The bottom line is avoid using Angular signals with Reactive Forms unless Angular adds built-in binding or a signal-based API for reactive forms.

Template vs Reactive Forms

Let's compare our code with that of template-driven forms. Although the template has more code there, it flows nicely. There is no duplication of code and everything just fits in.

Template Driven Forms:

Template Driven Forms Final Code

Reactive Forms:

Reactive Forms Final Code

So my verdict: The signals API seems to be made for template-driven forms - they go together perfectly. For now at least!

Conclusion

But that's just my two cents. What do you think? Let me know your thoughts as an Angular developer or otherwise below.

Check out my Angular and Firebase Authentication crash course

thumbnail
Angular Firebase Authentication: Create Full Sign Up App

Use Angular 16, Angular Material and Firebase Authentication, Firestore and Storage to create a complete Sign Up App!

You may also like...