Skip to content

Signal Forms / FormValueControl violates ControlValueAccessor contract by calling writeValue back after onChange, causing infinite loop #67847

@Nness

Description

@Nness

Which @angular/* package(s) are the source of the bug?

forms

Is this a regression?

No

Description

Signal forms break the one-way ControlValueAccessor contract by calling writeValue back on a CVA component after that component called onChange, directly contradicting Angular's own documentation.

The documented contract

Angular's official documentation at angular.dev states clearly:

writeValue(value: any): void
Writes a new value to the element.
This method is called by the forms API to write to the view when programmatic changes from model to view are requested.

This defines a strict one-way contract:

  • writeValue(value) — called by the form for programmatic, external, model-to-view changes only
  • onChange(value) — called by the component when the user changes the value (view-to-model)

These two directions must never form a loop. Angular's classic FormControl correctly respects this — calling onChange updates the FormControl's internal value but does not trigger writeValue back on the CVA. Signal forms do not respect this.

What Signal forms does wrong

When a CVA component bound via [formField] calls onChange(value), the Signal form updates its internal signal. Because Signal form has no concept of value origin — it does not distinguish between a programmatic external set() and a value that just arrived from the CVA via onChange — it reactively pushes the value back to writeValue on the same CVA that just sent it:

User types in CVA component
  → onChange("hello") called
    → Signal form signal updates
      → writeValue("hello") called back on CVA  ← violates documented contract

The same issue applies to FormValueControl, the Signal form equivalent of CVA. There is no mechanism to indicate who submitted the change — whether it originated from inside the component or from an external model update. Without that distinction, any reactive form implementation will produce this loopback.

Real-world impact

We implemented a CodeMirror editor as a ControlValueAccessor. CodeMirror manages its own internal document state. Our contract is:

  • writeValue = programmatic value from outside, push into CodeMirror
  • onChange = user edited the document, notify the form

When Signal form calls writeValue back after our onChange, we are forced to either:

  1. Compare the incoming writeValue value against CodeMirror's current document to detect the loopback — an O(n) string comparison on every keystroke. For large documents exceeding 1MB this causes severe and measurable performance degradation.
  2. Use a dirty flag to suppress the loopback writeValue — a workaround that should not be necessary and compensates for a broken contract.

Neither workaround is acceptable. FormControl does not require either. The behaviour difference between FormControl and Signal form for the same CVA implementation is a clear regression in the Signal form design.

Expected behaviour

Signal form must not call writeValue on a CVA in response to a value that was just received from that same CVA's onChange. This is the documented contract and the behaviour FormControl has always correctly implemented.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw


Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 21.2.3
Node: 22.21.1
Package Manager: npm 10.9.4
OS: Windows 11 x64

Angular: 21.2.5
... animations, cli, common, compiler, core, forms, localize, platform-browser, platform-browser-dynamic, router, sdk

Anything else?

No response

Metadata

Metadata

Assignees

Type

No type

Projects

Status

In Review

Relationships

None yet

Development

No branches or pull requests

Issue actions