From d6df9cbe31ae6eb3d5a754078a5c88eb0b6435c5 Mon Sep 17 00:00:00 2001 From: MVaik <105720462+MVaik@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:19:33 +0300 Subject: [PATCH] fix(react-form): fallback array field value during index change to prevent uncontrolled error --- packages/form-core/src/FieldApi.ts | 7 +- packages/react-form/tests/useField.test.tsx | 71 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 2af502b1b..21a269457 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -993,11 +993,16 @@ export class FieldApi< this.form = opts.form as never this.name = opts.name as never this.timeoutIds = {} as Record + // Only really relevant for arrays, the value could be gone once Derived's update fn is triggered + const potentialPreviousValue = this.form.getFieldValue(this.name) this.store = new Derived({ deps: [this.form.store], fn: () => { - const value = this.form.getFieldValue(this.name) + let value = this.form.getFieldValue(this.name) + if (value === undefined && potentialPreviousValue !== undefined) { + value = potentialPreviousValue + } const meta = this.form.getFieldMeta(this.name) ?? { ...defaultFieldMeta, ...opts.defaultMeta, diff --git a/packages/react-form/tests/useField.test.tsx b/packages/react-form/tests/useField.test.tsx index de58b87da..5f5781f79 100644 --- a/packages/react-form/tests/useField.test.tsx +++ b/packages/react-form/tests/useField.test.tsx @@ -1165,6 +1165,77 @@ describe('useField', () => { expect(queryByText(fakePeople.molly.name)).not.toBeInTheDocument() }) + it('should not make field uncontrolled during array item removal', async () => { + // Spy on console.error before rendering + const consoleErrorSpy = vi.spyOn(console, 'error') + + let id = 0 + + function Comp() { + const form = useForm({ + defaultValues: { + people: [] as { id: number; name: string }[], + }, + }) + + return ( +
+ + {(people) => ( +
+ + {people.state.value.map((person, i) => ( + + {(field) => { + return ( +
+ { + field.handleChange(e.target.value) + }} + /> + +
+ ) + }} +
+ ))} +
+ )} +
+
+ ) + } + + const { getByTestId } = render() + await user.click(getByTestId('add')) + await user.click(getByTestId('add')) + await user.click(getByTestId('remove-0')) + // Making a controlled field uncontrolled will log an error + expect(consoleErrorSpy).not.toHaveBeenCalled() + }) + it('should not rerender unrelated fields', async () => { const renderCount = { field1: 0,