Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
When the component that owns a signal form is destroyed, the form's fields aren't. Anything a field sets up for its own lifetime keeps going:
- An async validation request that's in flight isn't cancelled.
- Cleanup that a managed metadata key registers with
DestroyRefnever runs. - If the form was created with an environment injector, its validators keep reacting to model changes and start new requests after the injector is destroyed.
Fields that are removed from the model, such as deleted array items, are cleaned up correctly. Only the teardown of the whole form is missing.
Why it matters. Users who leave a page with a pending username check, address lookup, or similar validator keep that request running. Libraries that build on createManagedMetadataKey() can't rely on DestroyRef for cleanup, even though the field metadata guide says that cleanup is tied to the field's lifecycle.
Example. A form checks whether a username is available:
import {Component, resource, signal} from '@angular/core';
import {form, FormField, validateAsync} from '@angular/forms/signals';
@Component({
selector: 'signup-form',
imports: [FormField],
template: `<input [formField]="f.username" />`,
})
export class SignupForm {
readonly f = form(signal({username: ''}), (p) => {
validateAsync(p.username, {
params: ({value}) => value(),
factory: (username) =>
resource({
params: username,
loader: ({params, abortSignal}) =>
fetch(`/api/username-available?u=${params}`, {signal: abortSignal}).then((r) => r.json()),
}),
onSuccess: (available) => (available ? undefined : {kind: 'taken'}),
onError: () => undefined,
});
});
}The user types alice, and the form starts GET /api/username-available?u=alice. Before it returns, the user navigates away, and Angular destroys SignupForm.
Current behavior:
SignupForm destroyed
username field: still alive
GET /api/username-available?u=alice: still running (abortSignal.aborted === false)
Expected behavior (my assumption):
SignupForm destroyed
username field: destroyed
GET /api/username-available?u=alice: cancelled (abortSignal.aborted === true)
This assumes that a form's fields live as long as the injector the form was created with: the component's injector by default, or the injector option. Neither the API docs nor the guide says when a whole form's fields are destroyed, so this needs a maintainer decision. I've listed the open questions in the triage PR.
Please provide a link to a minimal reproduction of the bug
Failing specs: form_host_teardown.spec.ts in triage PR #71625. The PR changes no library code.
pnpm bazel test //packages/forms/signals/test/node:test --test_arg=--filter="form host teardown" --test_output=allPlease provide the exception or error you saw
There's no error. The specs fail with:
runs root-field DestroyRef callbacks when the form injector is destroyed
root-field DestroyRef callbacks after env.destroy(): Expected spy destroyed to have been called once. It was called 0 times.
runs live child-field DestroyRef callbacks when the form injector is destroyed
live child-field DestroyRef callbacks after env.destroy(): Expected spy destroyed to have been called once. It was called 0 times.
aborts an in-flight validateAsync load when the form injector is destroyed
loader AbortSignal.aborted after env.destroy(): Expected false to be true.
aborts an in-flight validateAsync load when the host component is destroyed
loader AbortSignal.aborted after fixture.destroy(): Expected false to be true.
starts no validation loads after the form injector is destroyed
loads started, including any after env.destroy(): Expected 2 to be 1.
Please provide the environment you discovered this bug in (run ng version)
angular/angular main at 3e173fc844c440775d3f110da8505442e227dee5 (2026-10-24), built from source
Test target: //packages/forms/signals/test/node:test
OS: macOS (darwin arm64)
Anything else?
Affected areas:
validateAsync(),validateHttp(), andvalidateStandardSchema()with async schemas: in-flight loads aren't aborted when the host is destroyed.- Custom managed metadata keys (
createManagedMetadataKey()):DestroyRefcallbacks,effect()s, andresource()s thatcreatesets up aren't cleaned up. - Forms created with an environment injector, for example through the
injectoroption: validation effects keep running, so new loads start after the injector is destroyed. compatForm()fields: they use a separate structure class that would need the same fix.- Not affected: fields removed from the model. The form's cleanup sweep destroys them, and the triage PR has a passing spec that shows it.
Fix options and tradeoffs. The PR has code for each option.
| Option | What changes | Tradeoffs |
|---|---|---|
| A (recommended): the form destroys all of its fields when its injector is destroyed | One DestroyRef hook per form, plus registering the root field |
About 15 lines. Defines the lifetime contract above. Doesn't define what happens if code reads a new field after teardown |
| B: each field's injector registers with the form's injector | A hook per field instead of per form | No bookkeeping, but more hooks on the host, and removing hooks during teardown needs care |
| C: replace the per-field injectors with a lightweight scope | A new small Injector type for fields |
Also removes the per-field injector cost in #71623. Much larger, and it touches dependency injection behavior |
Where to look in the code:
FieldNodeStructure.injectorcreates each field's injector as a child of the form's injector. A childR3Injectorisn't destroyed with its parent.FormFieldManager.createFieldManagementEffect()is the only code that destroys fields, and only removed ones.- The triage note has the full root cause, the behavior for each host type, and the spec output.
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsNo status
Activity
fix(forms): clean up fields when the form injector is destroyed