Skip to content

Commit 09307f6

Browse files
committed
fix: ensure async validation cancellation on field unmount
1 parent 8b41487 commit 09307f6

File tree

2 files changed

+68
-6
lines changed

2 files changed

+68
-6
lines changed

packages/form-core/src/FieldApi.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,12 +1863,13 @@ export class FieldApi<
18631863
promises: Promise<ValidationError | undefined>[],
18641864
) => {
18651865
const errorMapKey = getErrorMapKey(validateObj.cause)
1866-
const fieldValidatorMeta = field.getInfo().validationMetaMap[errorMapKey]
1866+
const fieldInfo = field.getInfo()
1867+
const fieldValidatorMeta = fieldInfo.validationMetaMap[errorMapKey]
18671868

18681869
fieldValidatorMeta?.lastAbortController.abort()
18691870
const controller = new AbortController()
18701871

1871-
this.getInfo().validationMetaMap[errorMapKey] = {
1872+
fieldInfo.validationMetaMap[errorMapKey] = {
18721873
lastAbortController: controller,
18731874
}
18741875

@@ -1877,11 +1878,11 @@ export class FieldApi<
18771878
let rawError!: ValidationError | undefined
18781879
try {
18791880
rawError = await new Promise((rawResolve, rawReject) => {
1880-
if (this.timeoutIds.validations[validateObj.cause]) {
1881-
clearTimeout(this.timeoutIds.validations[validateObj.cause]!)
1881+
if (field.timeoutIds.validations[validateObj.cause]) {
1882+
clearTimeout(field.timeoutIds.validations[validateObj.cause]!)
18821883
}
18831884

1884-
this.timeoutIds.validations[validateObj.cause] = setTimeout(
1885+
field.timeoutIds.validations[validateObj.cause] = setTimeout(
18851886
async () => {
18861887
if (controller.signal.aborted) return rawResolve(undefined)
18871888
try {
@@ -1911,14 +1912,20 @@ export class FieldApi<
19111912

19121913
const fieldLevelError = normalizeError(rawError)
19131914
const formLevelError =
1914-
asyncFormValidationResults[this.name]?.[errorMapKey]
1915+
asyncFormValidationResults[
1916+
field.name as keyof typeof asyncFormValidationResults
1917+
]?.[errorMapKey]
19151918

19161919
const { newErrorValue, newSource } =
19171920
determineFieldLevelErrorSourceAndValue({
19181921
formLevelError,
19191922
fieldLevelError,
19201923
})
19211924

1925+
if (field.getInfo().instance !== field) {
1926+
return resolve(undefined)
1927+
}
1928+
19221929
field.setMeta((prev) => {
19231930
return {
19241931
...prev,

packages/form-core/tests/FieldApi.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,6 +2229,61 @@ describe('field api', () => {
22292229
])
22302230
})
22312231

2232+
it('should cancel linked field async validation when the target field unmounts', async () => {
2233+
vi.useFakeTimers()
2234+
2235+
let resolve!: () => void
2236+
const promise = new Promise((r) => {
2237+
resolve = r as never
2238+
})
2239+
2240+
const form = new FormApi({
2241+
defaultValues: {
2242+
password: '',
2243+
confirm_password: '',
2244+
},
2245+
cleanupFieldsOnUnmount: true,
2246+
})
2247+
2248+
form.mount()
2249+
2250+
const passField = new FieldApi({
2251+
form,
2252+
name: 'password',
2253+
})
2254+
2255+
const passconfirmField = new FieldApi({
2256+
form,
2257+
name: 'confirm_password',
2258+
validators: {
2259+
onChangeListenTo: ['password'],
2260+
onChangeAsyncDebounceMs: 0,
2261+
onChangeAsync: async () => {
2262+
await promise
2263+
return 'Passwords do not match'
2264+
},
2265+
},
2266+
})
2267+
2268+
passField.mount()
2269+
const unmount = passconfirmField.mount()
2270+
2271+
passField.setValue('one')
2272+
await vi.runAllTimersAsync()
2273+
2274+
expect(unmount).toBeTypeOf('function')
2275+
unmount()
2276+
resolve()
2277+
await vi.runAllTimersAsync()
2278+
2279+
expect(form.state.fieldMeta.confirm_password).toMatchObject({
2280+
errors: [],
2281+
isValid: true,
2282+
})
2283+
2284+
vi.useRealTimers()
2285+
})
2286+
22322287
it('should add a new value to the fieldApi errorMap', () => {
22332288
interface Form {
22342289
name: string

0 commit comments

Comments
 (0)