Skip to content

Commit 8b41487

Browse files
committed
fix: coderabbit findings
1 parent 10abde0 commit 8b41487

3 files changed

Lines changed: 92 additions & 9 deletions

File tree

packages/form-core/src/FieldApi.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,10 @@ export class FieldApi<
13611361
const fieldInfo = this.form.fieldInfo[this.name]
13621362
if (!fieldInfo) return
13631363

1364+
// If a newer field instance has already been mounted for this name,
1365+
// avoid touching its shared validation state during teardown.
1366+
if (fieldInfo.instance !== this) return
1367+
13641368
for (const [key, validationMeta] of Object.entries(
13651369
fieldInfo.validationMetaMap,
13661370
)) {
@@ -1370,15 +1374,24 @@ export class FieldApi<
13701374
] = undefined
13711375
}
13721376

1373-
// If a newer field instance has already been mounted for this name,
1374-
// avoid clearing its state during teardown of an older instance.
1375-
if (fieldInfo.instance !== this) return
1376-
13771377
this.form.baseStore.setState((prev) => ({
1378+
// Preserve interaction flags so field-level defaultValue does not
1379+
// reseed user-entered values on remount.
13781380
...prev,
13791381
fieldMetaBase: {
13801382
...prev.fieldMetaBase,
1381-
[this.name]: defaultFieldMeta,
1383+
[this.name]: {
1384+
...defaultFieldMeta,
1385+
isTouched:
1386+
prev.fieldMetaBase[this.name]?.isTouched ??
1387+
defaultFieldMeta.isTouched,
1388+
isBlurred:
1389+
prev.fieldMetaBase[this.name]?.isBlurred ??
1390+
defaultFieldMeta.isBlurred,
1391+
isDirty:
1392+
prev.fieldMetaBase[this.name]?.isDirty ??
1393+
defaultFieldMeta.isDirty,
1394+
},
13821395
},
13831396
}))
13841397

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

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,7 +1671,7 @@ describe('field api', () => {
16711671

16721672
expect(form.getFieldValue('lastName')).toBe('abc')
16731673
expect(form.state.fieldMeta.lastName).toMatchObject({
1674-
isTouched: false,
1674+
isTouched: true,
16751675
isValid: true,
16761676
errors: [],
16771677
})
@@ -1688,10 +1688,39 @@ describe('field api', () => {
16881688

16891689
remountedLastName.mount()
16901690
expect(remountedLastName.getMeta().errors).toStrictEqual([])
1691-
expect(remountedLastName.getMeta().isTouched).toBe(false)
1691+
expect(remountedLastName.getMeta().isTouched).toBe(true)
16921692
expect(remountedLastName.getValue()).toBe('abc')
16931693
})
16941694

1695+
it('should preserve field-level defaultValue changes across unmount remount cleanup', () => {
1696+
const form = new FormApi({
1697+
defaultValues: {} as { name?: string },
1698+
cleanupFieldsOnUnmount: true,
1699+
})
1700+
1701+
form.mount()
1702+
1703+
const field = new FieldApi({
1704+
form,
1705+
name: 'name',
1706+
defaultValue: 'initial',
1707+
})
1708+
1709+
const unmount = field.mount()
1710+
field.setValue('changed')
1711+
expect(unmount).toBeTypeOf('function')
1712+
unmount()
1713+
1714+
const remountedField = new FieldApi({
1715+
form,
1716+
name: 'name',
1717+
defaultValue: 'initial',
1718+
})
1719+
1720+
remountedField.mount()
1721+
expect(remountedField.getValue()).toBe('changed')
1722+
})
1723+
16951724
it('should not apply in-flight async validation results after unmount', async () => {
16961725
vi.useFakeTimers()
16971726

@@ -1732,7 +1761,7 @@ describe('field api', () => {
17321761
await vi.runAllTimersAsync()
17331762

17341763
expect(form.state.fieldMeta.name).toMatchObject({
1735-
isTouched: false,
1764+
isTouched: true,
17361765
isValid: true,
17371766
errors: [],
17381767
})
@@ -1812,6 +1841,47 @@ describe('field api', () => {
18121841
expect(newField.getMeta().isTouched).toBe(true)
18131842
})
18141843

1844+
it('should not cancel newer instance async validation when older instance unmounts', async () => {
1845+
vi.useFakeTimers()
1846+
1847+
const form = new FormApi({
1848+
defaultValues: {
1849+
name: '',
1850+
},
1851+
cleanupFieldsOnUnmount: true,
1852+
})
1853+
1854+
form.mount()
1855+
1856+
const oldField = new FieldApi({
1857+
form,
1858+
name: 'name',
1859+
})
1860+
const oldUnmount = oldField.mount()
1861+
1862+
const newField = new FieldApi({
1863+
form,
1864+
name: 'name',
1865+
validators: {
1866+
onChangeAsyncDebounceMs: 10,
1867+
onChangeAsync: async ({ value }) =>
1868+
value === 'taken' ? 'name is taken' : undefined,
1869+
},
1870+
})
1871+
1872+
newField.mount()
1873+
newField.setValue('taken')
1874+
1875+
expect(oldUnmount).toBeTypeOf('function')
1876+
oldUnmount()
1877+
1878+
await vi.runAllTimersAsync()
1879+
1880+
expect(newField.getMeta().errors).toContain('name is taken')
1881+
1882+
vi.useRealTimers()
1883+
})
1884+
18151885
it('should show onSubmit errors', async () => {
18161886
const form = new FormApi({
18171887
defaultValues: {

packages/react-form/tests/useField.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ describe('useField', () => {
420420
await waitFor(() => expect(submitButton).toBeEnabled())
421421

422422
await user.click(submitButton)
423-
expect(onSubmit).toHaveBeenCalledTimes(1)
423+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
424424

425425
await user.clear(getByTestId('first-name'))
426426
await user.type(getByTestId('first-name'), 'a')

0 commit comments

Comments
 (0)