Skip to content

Commit f5be5a2

Browse files
committed
fix(core, react-form): Avoid set state error in React by avoiding setting default value until mount TanStack#1201
1 parent 135b886 commit f5be5a2

File tree

3 files changed

+65
-1
lines changed

3 files changed

+65
-1
lines changed

packages/form-core/src/FieldApi.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,10 @@ export interface FieldApiOptions<
494494
TFormOnServer,
495495
TParentSubmitMeta
496496
>
497+
/**
498+
* If true, the default value will not be set in the constructor.
499+
*/
500+
deferDefaultValue?: boolean
497501
}
498502

499503
export type FieldMetaBase<
@@ -996,7 +1000,7 @@ export class FieldApi<
9961000
this.form = opts.form as never
9971001
this.name = opts.name as never
9981002
this.timeoutIds = {} as Record<ValidationCause, never>
999-
if (opts.defaultValue !== undefined) {
1003+
if (!opts.deferDefaultValue && opts.defaultValue !== undefined) {
10001004
this.form.setFieldValue(this.name, opts.defaultValue as never, {
10011005
dontUpdateMeta: true,
10021006
})

packages/react-form/src/useField.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export function useField<
181181
...opts,
182182
form: opts.form,
183183
name: opts.name,
184+
deferDefaultValue: true, // Prevents https://react.dev/link/setstate-in-render by setting the default value after initial mount for React
184185
})
185186

186187
const extendedApi: typeof api &
@@ -204,6 +205,18 @@ export function useField<
204205

205206
useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])
206207

208+
useIsomorphicLayoutEffect(() => {
209+
if (fieldApi.options.defaultValue !== undefined) {
210+
fieldApi.form.setFieldValue(
211+
fieldApi.name,
212+
fieldApi.options.defaultValue as never,
213+
{
214+
dontUpdateMeta: true,
215+
},
216+
)
217+
}
218+
}, [fieldApi])
219+
207220
/**
208221
* fieldApi.update should not have any side effects. Think of it like a `useRef`
209222
* that we need to keep updated every render with the most up-to-date information.

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,4 +1130,51 @@ describe('useField', () => {
11301130
// field2 should not have rerendered
11311131
expect(renderCount.field2).toBe(field2InitialRender)
11321132
})
1133+
1134+
it('should handle defaultValue without setstate-in-render error', async () => {
1135+
// Spy on console.error before rendering
1136+
const consoleErrorSpy = vi.spyOn(console, 'error')
1137+
1138+
function Comp() {
1139+
const form = useForm({
1140+
defaultValues: {
1141+
fieldOne: '',
1142+
fieldTwo: '',
1143+
},
1144+
})
1145+
1146+
const fieldOne = useStore(form.store, (state) => state.values.fieldOne)
1147+
1148+
return (
1149+
<form>
1150+
<form.Field
1151+
name="fieldOne"
1152+
children={(field) => {
1153+
return (
1154+
<input
1155+
data-testid={field.name}
1156+
id={field.name}
1157+
value={field.state.value}
1158+
onChange={(e) => field.handleChange(e.target.value)}
1159+
/>
1160+
)
1161+
}}
1162+
/>
1163+
{fieldOne && (
1164+
<form.Field
1165+
name="fieldTwo"
1166+
defaultValue="default field two value"
1167+
children={(_) => null}
1168+
/>
1169+
)}
1170+
</form>
1171+
)
1172+
}
1173+
1174+
const { getByTestId } = render(<Comp />)
1175+
await user.type(getByTestId('fieldOne'), 'John')
1176+
1177+
// Should not log an error
1178+
expect(consoleErrorSpy).not.toHaveBeenCalled()
1179+
})
11331180
})

0 commit comments

Comments
 (0)