-
-
Notifications
You must be signed in to change notification settings - Fork 602
fix(core): field unmount #2068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(core): field unmount #2068
Changes from all commits
2afa535
29038e6
eb780f7
7dadaa7
8e9635c
9f7489e
b411f59
4eff6fa
10abde0
8b41487
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@tanstack/form-core': patch | ||
| '@tanstack/react-form': patch | ||
| --- | ||
|
|
||
| fix(core): field unmount |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1275,6 +1275,7 @@ export class FieldApi< | |
|
|
||
| /** | ||
| * Mounts the field instance to the form. | ||
| * @returns A function to unmount the field instance. | ||
| */ | ||
| mount = () => { | ||
| if (this.options.defaultValue !== undefined && !this.getMeta().isTouched) { | ||
|
|
@@ -1322,8 +1323,80 @@ export class FieldApi< | |
| fieldApi: this, | ||
| }) | ||
|
|
||
| // TODO: Remove | ||
| return () => {} | ||
| if (!this.form.options.cleanupFieldsOnUnmount) { | ||
| return () => {} | ||
| } | ||
|
|
||
| return () => { | ||
| // Stop any in-flight async validation or listener work tied to this instance. | ||
| for (const [key, timeout] of Object.entries( | ||
| this.timeoutIds.validations, | ||
| )) { | ||
| if (timeout) { | ||
| clearTimeout(timeout) | ||
| this.timeoutIds.validations[ | ||
| key as keyof typeof this.timeoutIds.validations | ||
| ] = null | ||
| } | ||
| } | ||
| for (const [key, timeout] of Object.entries(this.timeoutIds.listeners)) { | ||
| if (timeout) { | ||
| clearTimeout(timeout) | ||
| this.timeoutIds.listeners[ | ||
| key as keyof typeof this.timeoutIds.listeners | ||
| ] = null | ||
| } | ||
| } | ||
| for (const [key, timeout] of Object.entries( | ||
| this.timeoutIds.formListeners, | ||
| )) { | ||
| if (timeout) { | ||
| clearTimeout(timeout) | ||
| this.timeoutIds.formListeners[ | ||
| key as keyof typeof this.timeoutIds.formListeners | ||
| ] = null | ||
| } | ||
| } | ||
|
|
||
| const fieldInfo = this.form.fieldInfo[this.name] | ||
| if (!fieldInfo) return | ||
|
|
||
| // If a newer field instance has already been mounted for this name, | ||
| // avoid touching its shared validation state during teardown. | ||
| if (fieldInfo.instance !== this) return | ||
|
|
||
| for (const [key, validationMeta] of Object.entries( | ||
| fieldInfo.validationMetaMap, | ||
| )) { | ||
| validationMeta?.lastAbortController.abort() | ||
| fieldInfo.validationMetaMap[ | ||
| key as keyof typeof fieldInfo.validationMetaMap | ||
| ] = undefined | ||
| } | ||
|
Comment on lines
+1364
to
+1375
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Older-instance async validators can still write stale meta after a remount. The guard on Line 1366 prevents aborting the newer instance's shared controller, but it also skips aborting work started by the older instance. Those promises still commit through 🤖 Prompt for AI Agents |
||
|
|
||
| this.form.baseStore.setState((prev) => ({ | ||
| // Preserve interaction flags so field-level defaultValue does not | ||
| // reseed user-entered values on remount. | ||
| ...prev, | ||
| fieldMetaBase: { | ||
| ...prev.fieldMetaBase, | ||
| [this.name]: { | ||
| ...defaultFieldMeta, | ||
| isTouched: | ||
| prev.fieldMetaBase[this.name]?.isTouched ?? | ||
| defaultFieldMeta.isTouched, | ||
| isBlurred: | ||
| prev.fieldMetaBase[this.name]?.isBlurred ?? | ||
| defaultFieldMeta.isBlurred, | ||
| isDirty: | ||
| prev.fieldMetaBase[this.name]?.isDirty ?? | ||
| defaultFieldMeta.isDirty, | ||
| }, | ||
| }, | ||
| })) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| fieldInfo.instance = null | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Linked-field async work is not owned by the field being unmounted.
These loops only clear handles stored on
this, butvalidateAsyncstores linked-field work on the source field viathis.getInfo().validationMetaMap[...]on Line 1871 andthis.timeoutIds.validations[...]on Lines 1880-1884 even whenfield !== this. If a hidden field usesonChangeListenTo/onBlurListenTowith async validators, its teardown cannot cancel already-enqueued work and stale errors can still land after unmount. Please move timeout/controller ownership tofieldso the target field's cleanup can actually stop it.🤖 Prompt for AI Agents