The Problem
When you subscribe to a FormControl's valueChanges observable and then programmatically update the same control's value, the update triggers valueChanges again, creating an infinite loop.
Symptoms
- Browser freezes or crashes
- Maximum call stack size exceeded error
- Console fills with repeated log messages
- Form validation runs hundreds of times per second
Real Error Scenario
// Component
this.form.get('email').valueChanges.subscribe(value => {
if (value && !value.includes('@')) {
this.form.get('email').setValue(value + '@example.com'); // TRIGGERS valueChanges AGAIN!
}
});Every setValue call fires valueChanges, which calls setValue again, creating an infinite recursion.
How to Fix It
Fix 1: Use emitEvent: false
this.form.get('email').valueChanges.subscribe(value => {
if (value && !value.includes('@')) {
this.form.get('email').setValue(value + '@example.com', { emitEvent: false });
}
});emitEvent: false updates the value without triggering valueChanges.
Fix 2: Use patchValue with emitEvent
this.form.get('address').valueChanges.subscribe(addr => {
if (addr && addr.zipCode && !addr.city) {
this.form.patchValue({
city: this.lookupCity(addr.zipCode)
}, { emitEvent: false });
}
});Fix 3: Use distinctUntilChanged
this.form.get('email').valueChanges
.pipe(distinctUntilChanged())
.subscribe(value => {
if (value && !value.includes('@')) {
this.form.get('email').setValue(value + '@example.com', { emitEvent: false });
}
});Fix 4: Use takeUntil for Cleanup
```typescript private destroy$ = new Subject<void>();
ngOnInit() { this.form.get('email').valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => { this.form.get('email').setValue(this.normalize(value), { emitEvent: false }); }); }
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } ```
Fix 5: Custom Validator Instead of valueChanges
```typescript // Better approach: use a validator instead of valueChanges function emailDomainValidator(control: AbstractControl): ValidationErrors | null { const value = control.value; if (value && !value.includes('@')) { return { invalidEmail: true }; } return null; }
this.form = this.fb.group({ email: ['', [Validators.required, emailDomainValidator]] }); ```