Skip to content

Commit 4becd9b

Browse files
authored
Merge pull request #1 from ws-rush/feat/delayed-onmount-defaultvalues-10209780506199529847
Delay onMount until defaultValues are present
2 parents c61349e + c1e6d32 commit 4becd9b

File tree

11 files changed

+138
-4184
lines changed

11 files changed

+138
-4184
lines changed

docs/framework/react/guides/async-initial-values.md

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ As such, this guide shows you how you can mix-n-match TanStack Form with TanStac
1616

1717
## Basic Usage
1818

19+
TanStack Form automatically handles async initial values by delaying `onMount` listeners and validation until `defaultValues` are provided (i.e. not `undefined` or `null`).
20+
1921
```tsx
2022
import { useForm } from '@tanstack/react-form'
2123
import { useQuery } from '@tanstack/react-query'
2224

2325
export default function App() {
24-
const {data, isLoading} = useQuery({
26+
const {data} = useQuery({
2527
queryKey: ['data'],
2628
queryFn: async () => {
2729
await new Promise((resolve) => setTimeout(resolve, 1000))
@@ -30,22 +32,51 @@ export default function App() {
3032
})
3133

3234
const form = useForm({
33-
defaultValues: {
34-
firstName: data?.firstName ?? '',
35-
lastName: data?.lastName ?? '',
36-
},
35+
defaultValues: data,
3736
onSubmit: async ({ value }) => {
3837
// Do something with form data
3938
console.log(value)
4039
},
4140
})
4241

43-
if (isLoading) return <p>Loading..</p>
42+
// You can show a loading spinner while waiting for data
43+
// The form's onMount validation/listeners will NOT run until data is available
44+
if (!data) {
45+
return <p>Loading...</p>
46+
}
4447

4548
return (
46-
// ...
49+
<form.Provider>
50+
<form onSubmit={(e) => {
51+
e.preventDefault()
52+
e.stopPropagation()
53+
form.handleSubmit()
54+
}}>
55+
<form.Field
56+
name="firstName"
57+
children={(field) => (
58+
<input
59+
value={field.state.value}
60+
onBlur={field.handleBlur}
61+
onChange={(e) => field.handleChange(e.target.value)}
62+
/>
63+
)}
64+
/>
65+
<form.Field
66+
name="lastName"
67+
children={(field) => (
68+
<input
69+
value={field.state.value}
70+
onBlur={field.handleBlur}
71+
onChange={(e) => field.handleChange(e.target.value)}
72+
/>
73+
)}
74+
/>
75+
<button type="submit">Submit</button>
76+
</form>
77+
</form.Provider>
4778
)
4879
}
4980
```
5081

51-
This will show a loading spinner until the data is fetched, and then it will render the form with the fetched data as the initial values.
82+
In the example above, even though `useForm` is initialized immediately, the form validation and `onMount` effects will effectively "pause" until `data` is populated. This allows you to comfortably render a loading state without triggering premature validation errors or side effects.

packages/form-core/src/FieldApi.ts

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,16 @@ export class FieldApi<
11271127
formListeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
11281128
}
11291129

1130+
/**
1131+
* @private
1132+
*/
1133+
_hasMounted = false
1134+
1135+
/**
1136+
* @private
1137+
*/
1138+
_isMounted = false
1139+
11301140
/**
11311141
* Initializes a new `FieldApi` instance.
11321142
*/
@@ -1247,6 +1257,8 @@ export class FieldApi<
12471257
mount = () => {
12481258
const cleanup = this.store.mount()
12491259

1260+
this._isMounted = true
1261+
12501262
if (this.options.defaultValue !== undefined && !this.getMeta().isTouched) {
12511263
this.form.setFieldValue(this.name, this.options.defaultValue, {
12521264
dontUpdateMeta: true,
@@ -1258,41 +1270,10 @@ export class FieldApi<
12581270

12591271
this.update(this.options as never)
12601272

1261-
const { onMount } = this.options.validators || {}
1262-
1263-
if (onMount) {
1264-
const error = this.runValidator({
1265-
validate: onMount,
1266-
value: {
1267-
value: this.state.value,
1268-
fieldApi: this,
1269-
validationSource: 'field',
1270-
},
1271-
type: 'validate',
1272-
})
1273-
if (error) {
1274-
this.setMeta(
1275-
(prev) =>
1276-
({
1277-
...prev,
1278-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1279-
errorMap: { ...prev?.errorMap, onMount: error },
1280-
errorSourceMap: {
1281-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1282-
...prev?.errorSourceMap,
1283-
onMount: 'field',
1284-
},
1285-
}) as never,
1286-
)
1287-
}
1273+
return () => {
1274+
this._isMounted = false
1275+
cleanup()
12881276
}
1289-
1290-
this.options.listeners?.onMount?.({
1291-
value: this.state.value,
1292-
fieldApi: this,
1293-
})
1294-
1295-
return cleanup
12961277
}
12971278

12981279
/**
@@ -1343,6 +1324,49 @@ export class FieldApi<
13431324
if (!this.form.getFieldMeta(this.name)) {
13441325
this.form.setFieldMeta(this.name, this.state.meta)
13451326
}
1327+
1328+
if (
1329+
this._isMounted &&
1330+
!this._hasMounted &&
1331+
this.form.options.defaultValues !== undefined &&
1332+
this.form.options.defaultValues !== null
1333+
) {
1334+
this._hasMounted = true
1335+
1336+
const { onMount } = this.options.validators || {}
1337+
1338+
if (onMount) {
1339+
const error = this.runValidator({
1340+
validate: onMount,
1341+
value: {
1342+
value: this.state.value,
1343+
fieldApi: this,
1344+
validationSource: 'field',
1345+
},
1346+
type: 'validate',
1347+
})
1348+
if (error) {
1349+
this.setMeta(
1350+
(prev) =>
1351+
({
1352+
...prev,
1353+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1354+
errorMap: { ...prev?.errorMap, onMount: error },
1355+
errorSourceMap: {
1356+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1357+
...prev?.errorSourceMap,
1358+
onMount: 'field',
1359+
},
1360+
}) as never,
1361+
)
1362+
}
1363+
}
1364+
1365+
this.options.listeners?.onMount?.({
1366+
value: this.state.value,
1367+
fieldApi: this,
1368+
})
1369+
}
13461370
}
13471371

13481372
/**

packages/form-core/src/FormApi.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,16 @@ export class FormApi<
992992
*/
993993
private _devtoolsSubmissionOverride: boolean
994994

995+
/**
996+
* @private
997+
*/
998+
_hasMounted = false
999+
1000+
/**
1001+
* @private
1002+
*/
1003+
_isMounted = false
1004+
9951005
/**
9961006
* Constructs a new `FormApi` instance with the given form options.
9971007
*/
@@ -1396,11 +1406,11 @@ export class FormApi<
13961406
formEventClient.emit('form-unmounted', {
13971407
id: this._formId,
13981408
})
1399-
}
14001409

1401-
this.options.listeners?.onMount?.({ formApi: this })
1410+
this._isMounted = false
1411+
}
14021412

1403-
const { onMount } = this.options.validators || {}
1413+
this._isMounted = true
14041414

14051415
// broadcast form state for devtools on mounting
14061416
formEventClient.emit('form-api', {
@@ -1409,6 +1419,18 @@ export class FormApi<
14091419
options: this.options,
14101420
})
14111421

1422+
if (
1423+
this.options.defaultValues === undefined ||
1424+
this.options.defaultValues === null
1425+
) {
1426+
return cleanup
1427+
}
1428+
1429+
this.options.listeners?.onMount?.({ formApi: this })
1430+
this._hasMounted = true
1431+
1432+
const { onMount } = this.options.validators || {}
1433+
14121434
// if no validation skip
14131435
if (!onMount) return cleanup
14141436

@@ -1443,6 +1465,22 @@ export class FormApi<
14431465
// Options need to be updated first so that when the store is updated, the state is correct for the derived state
14441466
this.options = options
14451467

1468+
if (
1469+
this._isMounted &&
1470+
!this._hasMounted &&
1471+
options.defaultValues !== undefined &&
1472+
options.defaultValues !== null
1473+
) {
1474+
this.options.listeners?.onMount?.({ formApi: this })
1475+
this._hasMounted = true
1476+
1477+
const { onMount } = this.options.validators || {}
1478+
1479+
if (onMount) {
1480+
this.validateSync('mount')
1481+
}
1482+
}
1483+
14461484
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
14471485
const shouldUpdateReeval = !!options.transform?.deps?.some(
14481486
(val, i) => val !== this.prevTransformArray[i],

0 commit comments

Comments
 (0)