adev/src/content/guide/forms/signals/comparison.md
Angular provides three approaches to building forms: Signal Forms, Reactive Forms, and Template-driven Forms. Each has distinct patterns for managing state, validation, and data flow. This guide helps you understand the differences and choose the right approach for your project.
NOTE: Signal Forms are experimental as of Angular v21. The API may change before stabilizing.
| Feature | Signal Forms | Reactive Forms | Template-driven Forms |
|---|---|---|---|
| Source of truth | User-defined writable signal model | FormControl/FormGroup | User model in component |
| Type safety | Inferred from model | Explicit with typed forms | Minimal |
| Validation | Schema with path-based validators | List of validators passed to Controls | Directive-based |
| State management | Signal-based | Observable-based | Angular-managed |
| Setup | Signal + schema function | FormControl tree | NgModel in template |
| Best for | Signal-based apps | Complex forms | Simple forms |
| Learning curve | Medium | Medium-High | Low |
| Status | Experimental (v21+) | Stable | Stable |
The best way to understand the differences is to see the same form implemented in all three approaches.
<docs-code-multifile> <docs-code language="angular-ts" header="Signal forms" path="adev/src/content/examples/signal-forms/src/comparison/app/signal-forms.ts"/> <docs-code header="Reactive forms" path="adev/src/content/examples/signal-forms/src/comparison/app/reactive-forms.ts"/> <docs-code header="Template-driven forms" path="adev/src/content/examples/signal-forms/src/comparison/app/template-driven-forms.ts"/> </docs-code-multifile>The three approaches make different design choices that affect how you write and maintain your forms. These differences stem from where each approach stores form state and how it manages validation.
The most fundamental difference is where each approach considers the "source of truth" for form values.
Signal Forms stores data in a writable signal. When you need the current form values, you call the signal:
const credentials = this.loginModel(); // { email: '...', password: '...' }
This keeps your form data in a single reactive container that automatically notifies Angular when values change. The form structure mirrors your data model exactly.
Reactive Forms stores data inside FormControl and FormGroup instances. You access values through the form hierarchy:
const credentials = this.loginForm.value; // { email: '...', password: '...' }
This separates form state management from your component's data model. The form structure is explicit but requires more setup code.
Template-driven Forms stores data in component properties. You access values directly:
const credentials = {email: this.email, password: this.password};
This is the most direct approach but requires manually assembling values when you need them. Angular manages form state through directives in the template.
Each approach defines validation rules differently, affecting where your validation logic lives and how you maintain it.
Signal Forms uses a schema function where you bind validators to field paths:
loginForm = form(this.loginModel, (fieldPath) => {
required(fieldPath.email, {message: 'Email is required'});
email(fieldPath.email, {message: 'Enter a valid email address'});
});
All validation rules live together in one place. The schema function runs once during form creation, and validators execute automatically when field values change. Error messages are part of the validation definition.
Reactive Forms attaches validators when creating controls:
loginForm = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
});
Validators are tied to individual controls in the form structure. This distributes validation across your form definition. Error messages typically live in your template.
Template-driven Forms uses directive attributes in the template:
<input [(ngModel)]="email" required email />
Validation rules live in your template alongside the HTML. This keeps validation close to the UI but spreads logic across template and component.
TypeScript integration differs significantly between approaches, affecting how much the compiler helps you avoid errors.
Signal Forms infers types from your model structure:
const loginModel = signal({email: '', password: ''});
const loginForm = form(loginModel);
// TypeScript knows: loginForm.email exists and returns FieldState<string>
You define your data shape once in the signal, and TypeScript automatically knows what fields exist and their types. Accessing loginForm.username (which doesn't exist) produces a type error.
Reactive Forms requires explicit type annotations with typed forms:
const loginForm = new FormGroup({
email: new FormControl<string>(''),
password: new FormControl<string>(''),
});
// TypeScript knows: loginForm.controls.email is FormControl<string>
You specify types for each control individually. TypeScript validates your form structure, but you maintain type information separately from your data model.
Template-driven Forms offers minimal type safety:
email = '';
password = '';
// TypeScript only knows these are strings, no form-level typing
TypeScript understands your component properties but has no knowledge of form structure or validation. You lose compile-time checking for form operations.
To learn more about each approach: