Skip to content
Angular ng forms 4 min read

Reactive Forms Basics

Reactive forms give you a model-driven approach where the form’s structure and state live in TypeScript rather than in the template. You build an explicit tree of FormControl and FormGroup objects, bind them to the DOM, and read or react to their state as synchronous values and observable streams. This makes reactive forms predictable, strongly typed, and easy to unit test — which is why they’re the preferred choice for anything beyond a trivial input.

Why reactive forms

With template-driven forms, Angular builds the form model implicitly behind the scenes from directives like ngModel. Reactive forms invert that: you own the model. Because the source of truth is a plain object graph in your component, you can inspect it, mutate it, validate it, and test it without rendering a single template.

AspectReactive formsTemplate-driven forms
Source of truthComponent class (explicit)Template (implicit)
Data flowSynchronousAsynchronous
ValidationFunctions in codeDirectives in template
Scales to complex formsExcellentBecomes awkward
TestabilityHigh (no DOM needed)Requires DOM

Enabling reactive forms

Reactive forms live in ReactiveFormsModule. In a standalone component, import it directly into the component’s imports array — there’s no NgModule to register.

import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';

@Component({
  selector: 'app-newsletter',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './newsletter.component.html',
})
export class NewsletterComponent {
  email = new FormControl('');
}

A single FormControl

FormControl tracks the value and validation status of one input. You bind it to an element with the formControl directive, and Angular keeps the two in sync in both directions.

<label for="email">Email</label>
<input id="email" type="email" [formControl]="email" />

<p>Current value: {{ email.value }}</p>
@if (email.dirty) {
  <p>You changed this field.</p>
}

The constructor’s first argument is the initial value. Reading email.value returns it synchronously at any time, and the control exposes status flags like dirty, touched, valid, and pristine that update as the user interacts with the input.

The formControl directive (singular) binds one control. Don’t confuse it with formControlName (used inside a FormGroup) — mixing them up is the most common reactive-forms error.

Grouping with FormGroup

Real forms have several fields. FormGroup aggregates a collection of named controls into a single object whose value is a keyed record. Its value, validity, and dirty state are derived from its children.

import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-profile',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './profile.component.html',
})
export class ProfileComponent {
  profileForm = new FormGroup({
    firstName: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
    lastName: new FormControl('', { nonNullable: true }),
    email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }),
  });

  onSubmit(): void {
    if (this.profileForm.valid) {
      console.log('Submitting', this.profileForm.value);
    }
  }
}

In the template, bind the group with formGroup and each control by name with formControlName. Use the ngSubmit event on the <form> so Angular handles the submit cleanly.

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
  <input formControlName="firstName" placeholder="First name" />
  <input formControlName="lastName" placeholder="Last name" />
  <input formControlName="email" type="email" placeholder="Email" />

  @if (profileForm.get('email')?.hasError('email') && profileForm.get('email')?.touched) {
    <p class="error">Enter a valid email address.</p>
  }

  <button type="submit" [disabled]="profileForm.invalid">Save</button>
</form>

Submitting with a valid form logs the aggregated value:

Output:

Submitting { firstName: 'Ada', lastName: 'Lovelace', email: '[email protected]' }

Reading and reacting to state

Because value is synchronous, you can read the whole form at any moment. For reactions, every control and group exposes valueChanges and statusChanges as observables — ideal for live previews, autosave, or dependent fields.

import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `<input [formControl]="query" placeholder="Search..." />`,
})
export class SearchComponent {
  private destroyRef = inject(DestroyRef);
  query = new FormControl('', { nonNullable: true });

  constructor() {
    this.query.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((term) => console.log('Searching for:', term));
  }
}

Updating values from code

You control the model imperatively too. Use setValue to replace every field (it errors if any key is missing or extra) and patchValue to update a subset. reset returns the form to its initial state and clears the dirty/touched flags.

// Replace the entire form — all keys required
this.profileForm.setValue({ firstName: 'Grace', lastName: 'Hopper', email: '[email protected]' });

// Update only some fields
this.profileForm.patchValue({ email: '[email protected]' });

// Back to pristine, empty state
this.profileForm.reset();

Prefer getRawValue() over value when your form has disabled controls — value silently omits disabled fields, while getRawValue() returns them all.

Best practices

  • Import ReactiveFormsModule directly into standalone components that need it; keep imports local rather than global.
  • Use nonNullable: true on controls so reset() returns the initial value instead of null, and so the inferred type stays non-nullable.
  • Use formControl for lone inputs and formGroup + formControlName for grouped fields — never mix the two binding styles.
  • Drive validation messages off touched/dirty so users aren’t scolded before they’ve typed anything.
  • Unsubscribe from valueChanges with takeUntilDestroyed (or the async pipe) to avoid memory leaks.
  • For forms with many fields, reach for FormBuilder to cut down on boilerplate, and adopt typed forms for end-to-end type safety.
Last updated June 14, 2026
Was this helpful?