EnriqueMark
Angular

Reactive forms

2026-04 English

The biggest difference between angular’s design principles and react’s is that in react, state is basically everything, while angular wraps every feature up for you. signal looks a lot like state, but in practice you rarely seem to touch signal directly. The official design principle is to package up the best practices, and most of the time the official tools are enough on their own. Not like react, which hands you the smallest pieces and lets you assemble them yourself. signal actually came later, under react’s influence, but on the whole angular is still the big-and-complete kind, built mostly around pre-packaged tools.

Analogies are still a good way to carry knowledge over, though.

FormControl, start here. This one is a bit like react’s useState. Its main job is to define a single state value, which in react you update by hand, but here that’s all wrapped up for you.

//react style, you have to update with set yourself
const [username, setUsername] = useState('');
<input value={username} onChange={e => setUsername(e.target.value)} />

//angular style, updates automatically
usernameControl = new FormControl('');
<input [formControl]="usernameControl" />

Besides the first argument, which is the value, a FormControl object also takes one or more validator objects

//a single one, write it directly
username: new FormControl('', Validators.required)
//more than one, use a list
email: new FormControl('', [Validators.required, Validators.email]),

This is about the smallest object in a form. It has the properties below

PropertyMeaningTrue when
pristineoriginal statethe user hasn’t edited it
dirtydirty datathe user has edited it
untouchednot touchedthe user hasn’t visited it (never focused)
touchedtouchedthe user has visited it (after blur)
validpasses validationno validation errors
invalidfails validationhas validation errors
disableddisabledthe control is turned off
enabledenabledthe control is usable

FormGroup is a wrapper over a pile of FormControls, a group in the literal sense

//every one of them has to be declared explicitly
loginForm = new FormGroup({
  username: new FormControl(''),
  email: new FormControl(''),
  password: new FormControl('')
});
// reading the value
this.loginForm.value // { username: '', email: '', password: '' }
this.loginForm.get('username').value // ''
//in html you read the value through the attribute, then use formControlName for the matching key
<form [formGroup]="loginForm">
  <input formControlName="username" />
  <input formControlName="email" />
  <input formControlName="password" />
</form>

FormBuilder you can tell from the above too, using FormGroup directly is a real pain, so the official library gives you a factory class with everything wrapped up, one step and done.

// without FormBuilder
// fairly verbose
loginForm = new FormGroup({
  username: new FormControl('', Validators.required),
  email: new FormControl('', [Validators.required, Validators.email]),
});

// with FormBuilder
// done in one shot, this is also why the value is written as a list
// the logic here is no different from working with FormControl, value and validators (one or a list)
constructor(private fb: FormBuilder) {}
loginForm = this.fb.group({
  username: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
});

Validators is the validator. Plain react has no validators packaged up for you, you need an extra library, and without one you write your own hook. angular’s team built one in, but you can also write your own.

// custom validator, takes a control object and returns an error object or null
function forbiddenName(control: AbstractControl) {
  return control.value === 'admin' ? { forbiddenName: true } : null;
}
// same as the built-in ones, put the validator function in the validator slot
username = new FormControl('', [Validators.required, forbiddenName]);

valueChanges is like react’s side-effect function useEffect. It fires when the data you point it at changes, a side effect in the literal sense. The two do the same kind of thing here.

// react side-effect style
useEffect(() => {
  console.log('username changed:', username);
  // below is the data object it's bound to
}, [username]);

// below is how valueChanges does it, every Control object has this method
// and what it returns is an Observable, so you have to subscribe
// this one watches a single data object
this.usernameControl.valueChanges.subscribe(value => {
  console.log('username changed:', value);
});
// this one watches the whole form data object
this.loginForm.valueChanges.subscribe(formValue => {
  console.log('form changed:', formValue);
});

FormArray is the equivalent of an array state declared up front in react, except you can work on it with array-like methods, instead of the tedious business of copying with ... and then updating the whole state every time.

// this part is annoying, plain react means writing the logic for updating the entire list every time
const [phones, setPhones] = useState(['']);
const addPhone = () => setPhones([...phones, '']);
const removePhone = (i) => setPhones(phones.filter((_, idx) => idx !== i));

//defining it is similar, but this one wants a FormControl object inside
phoneNumbers = new FormArray([
  new FormControl('')
]);
addPhone() {
//this call really is more convenient
  this.phoneNumbers.push(new FormControl(''));
}
removePhone(i: number) {
  this.phoneNumbers.removeAt(i);
}

Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: 响应式表单.