In Angular reactive forms, what do a control's valueChanges, statusChanges and events Observables emit, and when does each one fire?
answer
- EventEmitter under the hood
- no value on subscribe
- programmatic changes count too
- emitEvent false silences
- events adds touched and pristine
basics
~20 svalueChanges emits the new value and statusChanges the recalculated status each time a control updates, from the UI or code, with no initial value; events emits typed ControlEvent objects for value, status, touched, pristine, submit and reset. None of them completes.
solid answer
~40 s`valueChanges` and `statusChanges` are `EventEmitter` instances the control creates, so they are hot, synchronous and **emit nothing on subscribe**. Both fire whenever the control runs `updateValueAndValidity()`: on user input, on `setValue()`, `patchValue()` and `reset()` from code, and on `enable()` or `disable()`. `statusChanges` emits `'VALID'`, `'INVALID'`, `'PENDING'` or `'DISABLED'` on every recalculation, even if the status did not change. Passing `{ emitEvent: false }` suppresses the emissions. `events`, added in Angular 18, is one stream of `ControlEvent` objects — `ValueChangeEvent`, `StatusChangeEvent`, `TouchedChangeEvent`, `PristineChangeEvent`, `FormSubmittedEvent` and `FormResetEvent` — each with a `source` control, which makes touched and pristine changes observable for the first time. None of the three ever completes.
code
ts · 28 linesimport { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-add-to-cart',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="addToCart">
<input type="number" formControlName="quantity" />
</form>
`,
})
export class AddToCart {
readonly addToCart = new FormGroup({
quantity: new FormControl(1, { nonNullable: true, validators: [Validators.min(1)] }),
variant: new FormControl('red', { nonNullable: true }),
});
constructor() {
const quantity = this.addToCart.controls.quantity;
// Nothing logs here on subscribe: valueChanges has no initial value.
quantity.valueChanges.subscribe((q) => console.log('quantity', q));
quantity.statusChanges.subscribe((s) => console.log('status', s));
quantity.setValue(3); // logs quantity 3, status VALID
quantity.setValue(4, { emitEvent: false }); // logs nothing
}
}go deeper
Recall that valueChanges emits on each change but not on subscribe, and that code-driven setValue() calls also emit.
Explain what triggers the streams, emitEvent: false, statusChanges emitting on every recalculation, and the parent timing caveat.
Use the events stream for touched and submit state, prevent feedback loops between controls, and keep long-lived subscriptions under control.
Decide when a form's reactive logic should stay on these streams and when moving to Signal Forms is worth the migration.
## The three streams on every control Every `AbstractControl` — `FormControl`, `FormGroup`, `FormArray` — exposes three Observables. On a product-detail page, think of a `quantity` control and a `variant` control inside an `addToCart` group. This answer assumes Angular 22.2. | Stream | Type of value | Backed by | Emits on subscribe? | | --- | --- | --- | --- | | `valueChanges` | The control's value | `EventEmitter` | No | | `statusChanges` | `FormControlStatus` | `EventEmitter` | No | | `events` | `ControlEvent` subclasses | A `Subject`, exposed with `asObservable()` | No | None of them completes: they live as long as the control. ## When valueChanges and statusChanges fire Both are emitted at the end of `updateValueAndValidity()`, which runs: - when the user types or picks a value, according to the control's `updateOn` setting (`'change'` by default, or `'blur'` or `'submit'`); - when code calls `setValue()`, `patchValue()` or `reset()`; - when code calls `enable()` or `disable()`; - when validators are re-run explicitly with `updateValueAndValidity()`. Details that trip people up: 1. **No initial value.** Subscribing emits nothing until the next change. If you need the current value first, start the stream with it yourself. 2. **Programmatic changes count.** A `setValue()` in a subscriber of another control's `valueChanges` emits too, which is how two controls end up updating each other in a loop. 3. **`emitEvent: false`.** Passing `{ emitEvent: false }` to `setValue()`, `patchValue()`, `enable()` and similar updates the control silently. 4. **Status on every recalculation.** `statusChanges` emits each time the status is recomputed, even when it was `'VALID'` before and is `'VALID'` again. 5. **Parent timing.** A child emits right after its own value updates, before its parent group's `value` is updated. Reading `addToCart.value` inside the quantity control's `valueChanges` callback can show the old quantity; subscribe to the group instead. ## The unified events stream Angular 18 added `events`, a single stream of typed events: - `ValueChangeEvent` — the new `value`; - `StatusChangeEvent` — the new `status`; - `TouchedChangeEvent` — `touched` became true or false; - `PristineChangeEvent` — `pristine` became true or false; - `FormSubmittedEvent` — the form directive handled a submit; - `FormResetEvent` — the control was reset. Each event carries `source`, the control where the change started. Before this API, touched and pristine changes had no Observable at all. `emitEvent: false` suppresses these events too. ```ts this.addToCart.events .pipe(filter((e) => e instanceof TouchedChangeEvent)) .subscribe((e) => console.log('touched changed on', e.source)); ``` ## Choosing between them - Need just the value, as for a live price preview? `valueChanges`. - Need validity for enabling a button outside the form? `statusChanges`, or read `status` in the template. - Need touched, pristine, submit or reset notifications? `events`. ## What they are not - They are not signals: reading them in a template requires the async pipe or a conversion. - They are not cold: every subscriber shares the same emissions, and a late subscriber misses earlier ones. - They do not complete when the component is destroyed; a manual subscription stays open for the control's life.
- Why can reading the parent group's value inside a child's valueChanges callback show stale data?The child emits right after its own value updates, and the parent recalculates its value afterwards as the update propagates upward. Inside the child's callback the group's `value` may still hold the old child value; subscribing to the group's `valueChanges` gives the updated whole.
- How do you react to a control becoming touched?Subscribe to the control's `events` and filter for `TouchedChangeEvent`, which reports `touched` and the `source` control. `valueChanges` and `statusChanges` never emit for touched or pristine changes.
saying these in an interview costs you the question
- valueChanges emits the current value as soon as you subscribe.
- valueChanges fires only for user input, not for setValue() calls.
- statusChanges emits only when the status actually changes.
- Form Observables complete when the component is destroyed.
- valueChanges also reports touched and pristine changes.