Skip to content

fix(forms): clean up fields when the form injector is destroyed - #71754

Open
splincode wants to merge 1 commit into
angular:mainfrom
splincode:fix/signal-form-host-teardown
Open

splincode wants to merge 1 commit into
angular:mainfrom
splincode:fix/signal-form-host-teardown

Conversation

@splincode

@splincode splincode commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

PR Checklist

  • The commit message follows the guidelines.
  • Tests have been added.
  • The injector option documents resource cleanup.

PR Type

  • Bugfix

What is the current behavior?

Destroying a form's component or environment injector leaves its field injectors alive. Pending async validation is not aborted, managed metadata cleanup does not run, and environment-owned validation can start another request after teardown.

Fixes #71622.

What is the new behavior?

The field manager registers one destroy callback on the form's chosen injector and destroys its remaining fields. Root and compat structures join the existing registry. Removed fields are already deleted from that registry, and destroying an unused field does not allocate an injector.

This implements the manager-based approach described in the issue. An explicitly supplied injector remains the owner: destroying the calling component does not end the form's lifetime.

Does this PR introduce a breaking change?

  • No

Public API signatures and update scheduling are unchanged. The intended behavior change is cleanup of resources that previously survived their owner.

Validation

On main at 74961c662420ea02a5872bfa1a0ee3284526b887, all eight new lifecycle specs fail. With the fix:

  • //packages/forms/signals/test/node:test: 561 specs, 0 failures.
  • //packages/forms/signals/test/web:test_chromium: 362 specs, 0 failures.
  • ng-dev format, Prettier and git diff --check pass.

Coverage includes root and child cleanup before the first effect tick, component teardown, compat field injectors, exactly-once cleanup after removal, aborting pending validation, no validation loads after owner destruction, and the external-injector lifetime.

Local Bazel runs use telemetry opt-out, the system Java CA trust store, and a local test-runner workaround that supplies loopback only if this container rejects os.networkInterfaces(). No test assertions or Angular behavior are altered by that workaround. The full monorepo suite was not run.

The lazy-destroy guard also appears in #71737; it is needed here to avoid creating injectors during owner teardown.

Production-mode memory measurement

Same AOT/ES2022/minified signup fixture built from base 74961c662420ea02a5872bfa1a0ee3284526b887 and PR head 1726af8bba3b3c1f1a31221698e919071ff66c74, with ngDevMode=false and ngJitMode=false. One username input, validateAsync(resource(fetch(...))), and 100 open/close cycles through @if.

Seven alternating pairs per scenario, fresh contexts, 20 completed-request warmup cycles. Metric: increase in Runtime.getHeapUsage().usedSize over the post-warmup heap, after three forced GCs. Chromium 158.0.8117.0, Linux x64. Medians:

After 100 destroyed forms Base PR
Heap increase while validation is pending 3.144 MiB 0.131 MiB
Completed-before-navigation control 0.148 MiB 0.146 MiB
Pending case after letting old requests finish 0.131 MiB 0.132 MiB
Pending validator loads not aborted on teardown 100 0

Median paired reduction: 3.012 MiB (3,158,788 bytes; observed range 3,157,976–3,159,336 bytes). All 100 pending loads were aborted in every PR sample. Counts describe loader/fetch invocations; Chromium can queue/coalesce local HTTP/1.1 requests.

This is a controlled production-mode fixture, not a deployed application. The completed control and response-release check show retention while validation is pending; this is not a claim of permanent component leakage after responses arrive or of an RSS/peak-memory reduction.

Reproduction bundles, fixture source, raw samples and build hashes:

angular-71754-memory-evidence.zip

Destroy remaining field injectors with the form's chosen injector so pending
validation is aborted and managed metadata cleanup runs. Register root and
compat structures for teardown while preserving cleanup of removed fields
and the lifetime of an explicitly provided external injector.

Fixes angular#71622
@ngbot ngbot Bot added this to the Backlog milestone Oct 31, 2026
@pullapprove
pullapprove Bot requested a review from kirjs October 31, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signal forms: destroying a form's component doesn't destroy its fields, so async validation keeps running

1 participant