Angular 22 lands this release window with what is arguably its biggest API shift since standalone components: Signal Forms graduates from experimental to stable. The new form() API replaces the verbose FormGroup / FormControl ceremony with a typed, signal-backed model where every field, value, validity state, and error reads through the same primitives you already use for component state.
For teams that have been waiting for "the signal era" to settle before committing, that day has arrived. Angular 22 also makes zoneless change detection the default for new projects and flips new components to OnPush out of the box, so Signal Forms is part of a broader push to make Angular reactivity coherent end-to-end rather than a separate dialect bolted onto Zone.js.
This post is a practical walkthrough: what the API looks like, how validation and submission work, where it differs from Reactive Forms, and what to migrate first.
What "Signal Forms" actually is
Signal Forms represents form state as a writable signal you own, plus a form() wrapper that exposes a tree of fields with their own signals for value, touched state, errors, and validity. There is no FormBuilder injection, no string-keyed get('user.email'), no separate valueChanges observable to subscribe to. Reading a field's value is just calling a signal; writing it updates your model signal directly.
The shortest possible login form looks like this (Angular docs):
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
interface LoginData {
email: string;
password: string;
}
@Component({
selector: 'app-login',
templateUrl: './login.html',
imports: [FormField],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginComponent {
protected readonly model = signal<LoginData>({ email: '', password: '' });
protected readonly loginForm = form(this.model);
}
<form>
<label>
Email
<input type="email" [formField]="loginForm.email" />
</label>
<label>
Password
<input type="password" [formField]="loginForm.password" />
</label>
<p>Password length: {{ loginForm.password().value().length }}</p>
</form>
A few things worth pointing out:
- The types come from your model.
loginForm.emailonly exists becauseLoginDatahas anemailfield. Rename the property and the template breaks at compile time, not at runtime. - The field exposes signals, not properties.
loginForm.email()returns aFieldStateobject whose own members —value(),touched(),errors(),valid()— are signals you read in templates or computed values. - The state is in your signal, not the form. The form is a view over the model. Updating
model.set({...})updates the form; resetting is just callingmodel.set(initialValue).
Validation, finally, looks like the rest of Angular
Reactive Forms validators were functions that returned ValidationErrors | null, lived outside the reactive graph, and required updateValueAndValidity() calls when their conditions changed. In Signal Forms, validators run inside a reactive context, so they automatically re-evaluate when any signal they read changes — including signals from other fields.
import { form, required, minLength, validate, customError } from '@angular/forms/signals';
protected readonly signupForm = form(this.model, (path) => {
required(path.email, { message: 'Email is required' });
required(path.password, { message: 'Password is required' });
minLength(path.password, 12, {
message: 'Use at least 12 characters',
});
validate(path.passwordConfirm, ({ valueOf }) => {
return valueOf(path.password) === valueOf(path.passwordConfirm)
? null
: customError({ kind: 'mismatch', message: 'Passwords do not match' });
});
});
Because validate is just reading signals through valueOf, the confirm-password rule re-runs whenever either field changes — no manual wiring required. Cross-field rules, conditional required, "this field is required only when that other field is X" — all of it composes the same way.
Angular 22 also ships first-class async validation through validateHttp() and validateAsync(), with pending() and submitting() signals you can read directly to drive loading UI (Async operations docs).
Submission stops feeling like a workaround
Reactive Forms never had an opinionated submit lifecycle. You wrote your own (ngSubmit) handler, checked form.valid, called your service, and managed a submitting flag by hand. Signal Forms provides submit() and a submitting() signal that the framework manages for you:
import { form, submit } from '@angular/forms/signals';
protected readonly loginForm = form(this.model, validators);
async onSubmit() {
await submit(this.loginForm, async ({ value }) => {
await this.auth.signIn(value());
});
}
If synchronous validation fails, submit does not invoke your action at all. While the action runs, loginForm().submitting() returns true, which makes disabling the button a one-liner:
<button type="submit" [disabled]="loginForm().submitting() || !loginForm().valid()">
Sign in
</button>
By default, pending async validators do not block submission, which matches the common case where you have already shown the user a "checking..." indicator. If your flow needs every async validator to settle first, you can opt into the 'none' mode for pending validators on the submit call (Form submission docs).
Why this matters beyond ergonomics
The headline pitch is "less boilerplate," but the bigger win is alignment with the rest of the framework. With Signal Forms:
- Change detection is precise. Updating one field in a 50-field form does not invalidate the whole tree.
OnPushis no longer a footgun for forms. Templates re-render only the field signals they actually read.- Forms participate in
computed()andeffect()withoutvalueChanges.pipe(...). Derived state — totals, summaries, dependent dropdowns — is just a computed signal. - Tests get simpler. You drive
model.set(...), readloginForm.email().errors(), and assert. NoTestBed.fakeAsync(() => tick())forvalueChangesemissions.
Combined with zoneless being the default in Angular 22, this is the first Angular release where you can build a non-trivial form-heavy app without touching Zone.js, RxJS for form plumbing, or the legacy template-driven directives.
What to migrate first
@angular/forms/signals lives alongside @angular/forms. The old APIs are not deprecated, and the Angular team has been explicit that both will coexist for the foreseeable future. So the practical migration path is incremental:
- New screens, new API. Any form you write from scratch in Angular 22 should use
form(). The DX delta is large enough that mixing styles inside the same screen is a worse experience than committing. - Lift the model first. When migrating an existing form, start by introducing a
signal<FormShape>that mirrors the currentFormGroupvalue. Wire it both ways for a release, then swap the template to[formField]once the model is the source of truth. - Multi-step flows benefit most. Wizards and conditional forms — the ones where Reactive Forms forces awkward
setValidatorscalls and manualupdateValueAndValidity()— get dramatically simpler. Migrate those before you touch the plain CRUD forms. - Hold off on shared
FormBuilderhelpers. If your codebase has a customFormBuilderwrapper, do not port it. The new API is small enough that the wrapper is the abstraction that no longer earns its keep.
What to leave alone, for now: dynamic forms generated from JSON schemas, anything that relies heavily on AbstractControl inheritance from custom control classes, and any place you depend on the exact emission order of valueChanges. The new API has answers for all of these, but the migration cost is highest there.
Takeaways
- Angular 22 promotes Signal Forms to stable, replacing the imperative
FormGroupmodel with a typed, signal-backedform()API. - Validation, async checks, and submission are first-class — no more handwritten submitting flags or cross-field hacks.
- Combined with zoneless-by-default and
OnPush-by-default, this is the release where Angular's reactivity story stops having two dialects. - Migrate incrementally: new forms first, then wizards and conditional flows, then the boring CRUD pages. The old API is not going anywhere yet.
If your team has been deferring an Angular upgrade because forms felt like the most painful part of the framework, Angular 22 is the release that earns the rewrite of your worst-offending screens.