Skip to content

Signal forms: every app that uses form() ships AbstractControl and Validators from reactive forms #71621

Open

Description

@Flarette

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

forms

Is this a regression?

No

Description

form() includes a safety check: if a rule reads another field with valueOf() and that field's value is a reactive forms control, it throws NG1907 and suggests compatForm(). The check uses instanceof AbstractControl. That reference alone keeps AbstractControl in the production bundle, and AbstractControl brings Validators, the control event classes, and parts of RxJS with it. This happens in every app that calls form(), including apps that never import reactive forms.

Why it matters. Apps that use only signal forms pay for reactive forms code they never run. A minimal app pays about 4.7 KB gzipped (10.7%) for code it never runs. An app that binds fields with [formField] pays about 1.9 KB (3.3%).

Example. A minimal app that uses only signal forms:

import {Component, provideZonelessChangeDetection, signal} from '@angular/core';
import {form, required} from '@angular/forms/signals';
import {bootstrapApplication} from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  template: `{{ f.name().value() }} {{ f.name().valid() }}`,
})
class App {
  readonly f = form(signal({name: ''}), (p) => {
    required(p.name);
  });
}

bootstrapApplication(App, {providers: [provideZonelessChangeDetection()]});

Current behavior (production build, gzipped main.js):

44,490 B
includes AbstractControl, Validators, the control event classes, RxJS forkJoin and from

Expected behavior (my assumption):

39,742 B
without AbstractControl, Validators, the control event classes, or RxJS forkJoin and from

The expected size is what I measured with the check limited to development builds. This assumes that the NG1907 check is a development aid that doesn't need to run in production, which is how many Angular diagnostics work. If the team wants it in production, a check that doesn't reference the class gives the same size. I've listed this decision in the triage PR.

Please provide a link to a minimal reproduction of the bug

Measurement apps: forms_signals_model and forms_signals_field, in triage PR #71624. The PR changes no library code.

pnpm bazel build //packages/core/test/bundling/forms_signals_model:bundles //packages/core/test/bundling/forms_signals_field:bundles

Please provide the exception or error you saw

There's no error. These are the production bundle sizes in bytes:

App            Today (raw / gzip / brotli)    Check in dev mode only (raw / gzip / brotli)    Saved (gzip)
model only     141,563 / 44,490 / 39,869      124,217 / 39,742 / 35,722                       4,748 B (10.7%)
[formField]    182,983 / 56,835 / 50,450      173,946 / 54,939 / 48,812                       1,896 B (3.3%)

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

angular/angular main at 3e173fc844c440775d3f110da8505442e227dee5 (2026-10-24), built from source
Build: optimize_angular_app (production Angular CLI build), default settings
OS: macOS (darwin arm64)

Anything else?

Affected areas:

  • Every app that calls form(), whether or not any rule calls valueOf().
  • Apps that use [formField] gain less from a fix: they still ship Validators, NgControl, and a few other reactive forms classes through the ControlValueAccessor interop. That's a separate, larger cleanup.
  • Apps that use compatForm() already depend on reactive forms, so they gain nothing.
  • With the recommended fix, the NG1907 error throws only in development. In production, a rule that reads a FormControl through valueOf() would get the control back instead of an error.

Fix options and tradeoffs. The PR has code for each option.

Option What changes Tradeoffs
A (recommended): check only in development if (ngDevMode && result instanceof AbstractControl) One line, measured. Development keeps the error, production doesn't
B: check with a private helper from @angular/forms A symbol on AbstractControl and a private ɵisAbstractControl() export Keeps the production error. Adds a cross-package private API. Not measured
C: remove the check Delete it Smallest change, but it loses the diagnostic everywhere

Where to look in the code:

  • FieldNodeContext.valueOf holds the check.
  • The triage note has the full measurements, the list of names that ship only because of the check, and the full code for each option.

Activity

  1. added this to the needsTriage milestone on Oct 26, 2026
  2. added this to the needsTriage milestone on Oct 26, 2026
  3. added a commit that references this issue on Nov 2, 2026
    3e96a2b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions