diff --git a/.changeset/quiet-ids-return.md b/.changeset/quiet-ids-return.md new file mode 100644 index 0000000000..2700903c5b --- /dev/null +++ b/.changeset/quiet-ids-return.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-form': patch +'@tanstack/preact-form': patch +--- + +Fix an infinite render loop in `useForm` and `useAppForm` when `formId` changes from a defined value to `undefined`. The form now falls back to its generated `formId` instead. diff --git a/packages/preact-form/src/useForm.tsx b/packages/preact-form/src/useForm.tsx index ca50d07d4c..58e7e064d2 100644 --- a/packages/preact-form/src/useForm.tsx +++ b/packages/preact-form/src/useForm.tsx @@ -200,7 +200,7 @@ export function useForm< >, ) { const fallbackFormId = useFormId() - const [prevFormId, setPrevFormId] = useState(opts?.formId as never) + const [prevFormId, setPrevFormId] = useState(opts?.formId) const [formApi, setFormApi] = useState(() => { return new FormApi< @@ -222,7 +222,7 @@ export function useForm< if (prevFormId !== opts?.formId) { const formId = opts?.formId ?? fallbackFormId setFormApi(new FormApi({ ...opts, formId })) - setPrevFormId(formId) + setPrevFormId(opts?.formId) } const extendedFormApi = useMemo(() => { diff --git a/packages/preact-form/tests/useForm.test.tsx b/packages/preact-form/tests/useForm.test.tsx index 1bc951aa82..1b6e5d2a90 100644 --- a/packages/preact-form/tests/useForm.test.tsx +++ b/packages/preact-form/tests/useForm.test.tsx @@ -898,6 +898,33 @@ describe('useForm', () => { expect(result).toHaveTextContent('1') }) + it('should restore the generated formId when formId changes back to undefined', () => { + let renderCount = 0 + + function Comp({ formId }: { formId?: string }) { + renderCount++ + if (renderCount > 50) { + throw new Error('Render loop detected') + } + + const form = useForm({ formId }) + + return {form.formId} + } + + const { getByTestId, rerender } = render() + const generatedFormId = getByTestId('formId').textContent + + rerender() + expect(getByTestId('formId').textContent).toBe('test') + + rerender() + expect(getByTestId('formId').textContent).toBe(generatedFormId) + + rerender() + expect(getByTestId('formId').textContent).toBe('test') + }) + it('should allow custom component keys for arrays', async () => { function Comp() { const form = useForm({ diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 2b898adca6..69a8a00630 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -201,7 +201,7 @@ export function useForm< >, ) { const fallbackFormId = useFormId() - const [prevFormId, setPrevFormId] = useState(opts?.formId as never) + const [prevFormId, setPrevFormId] = useState(opts?.formId) const [formApi, setFormApi] = useState(() => { return new FormApi< @@ -223,7 +223,7 @@ export function useForm< if (prevFormId !== opts?.formId) { const formId = opts?.formId ?? fallbackFormId setFormApi(new FormApi({ ...opts, formId })) - setPrevFormId(formId) + setPrevFormId(opts?.formId) } const extendedFormApi = useMemo(() => { diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx index b71094cddc..18b38e753f 100644 --- a/packages/react-form/tests/useForm.test.tsx +++ b/packages/react-form/tests/useForm.test.tsx @@ -893,6 +893,26 @@ describe('useForm', () => { expect(result).toHaveTextContent('1') }) + it('should restore the generated formId when formId changes back to undefined', () => { + function Comp({ formId }: { formId?: string }) { + const form = useForm({ formId }) + + return {form.formId} + } + + const { getByTestId, rerender } = render() + const generatedFormId = getByTestId('formId').textContent + + rerender() + expect(getByTestId('formId').textContent).toBe('test') + + rerender() + expect(getByTestId('formId').textContent).toBe(generatedFormId) + + rerender() + expect(getByTestId('formId').textContent).toBe('test') + }) + it('should allow custom component keys for arrays', async () => { function Comp() { const form = useForm({