diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 7624b7c7a..1a3291819 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1175,9 +1175,16 @@ export class FieldApi< // The name is dynamic in array fields. It changes when the user performs operations like removing or reordering. // In this case, we don't want to force a default value if the store managed to find an existing value. if (nameHasChanged) { - this.setValue((val) => (val as unknown) || defaultValue, { - dontUpdateMeta: true, - }) + this.setValue( + (val) => { + // Preserve falsy values used for checkboxes or textfields (e.g. false, '') + const newValue = val !== undefined ? val : defaultValue + return newValue + }, + { + dontUpdateMeta: true, + }, + ) } else if (defaultValue !== undefined) { this.setValue(defaultValue as never, { dontUpdateMeta: true, diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index cfd292e46..4f5bc9a09 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1392,7 +1392,7 @@ export class FormApi< const errorMapKey = getErrorMapKey(validateObj.cause) for (const field of Object.keys( - this.state.fieldMeta, + this.baseStore.state.fieldMetaBase, // Iterate over the field meta base since it is the source of truth ) as DeepKeys[]) { const fieldMeta = this.getFieldMeta(field) if (!fieldMeta) continue @@ -1569,7 +1569,7 @@ export class FormApi< const errorMapKey = getErrorMapKey(validateObj.cause) for (const field of Object.keys( - this.state.fieldMeta, + this.baseStore.state.fieldMetaBase, // Iterate over the field meta base since it is the source of truth ) as DeepKeys[]) { const fieldMeta = this.getFieldMeta(field) if (!fieldMeta) continue diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 38e771a1e..878277083 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -478,7 +478,20 @@ export const Field = (< const fieldApi = useField(fieldOptions as any) const jsxToDisplay = useMemo( - () => functionalUpdate(children, fieldApi as any), + () => { + /** + * When field names switch field store and form store are out of sync. + * When in this state, React should not render the component + */ + const isFieldStoreOutofSync = + fieldApi.state.value !== fieldApi.form.getFieldValue(fieldOptions.name) + + if (isFieldStoreOutofSync) { + return null + } + + return functionalUpdate(children, fieldApi as any) + }, /** * The reason this exists is to fix an issue with the React Compiler. * Namely, functionalUpdate is memoized where it checks for `fieldApi`, which is a static type. diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx index ac62ed80b..2cb306468 100644 --- a/packages/react-form/tests/useForm.test.tsx +++ b/packages/react-form/tests/useForm.test.tsx @@ -794,4 +794,103 @@ describe('useForm', () => { expect(fn).toHaveBeenCalledTimes(1) }) + + it('preserves empty string values when removing array elements', async () => { + function Comp() { + const form = useForm({ + defaultValues: { + interests: [{ interestName: '', id: 0 }], + }, + }) + return ( +
+ + {(interestsFieldArray) => ( +
+ + + {interestsFieldArray.state.value.map((row, i) => { + return ( + + {(field) => { + return ( +
+ { + field.handleChange(e.target.value) + }} + /> + +
+ ) + }} +
+ ) + })} +
+

{JSON.stringify(form.getFieldValue('interests'))}

+
+ +
+ )} +
+
+ ) + } + + const { getByTestId } = render() + + // Add 2 interests + await user.click(getByTestId('add-interest')) + await user.click(getByTestId('add-interest')) + + // Remove the first interest + await user.click(getByTestId('remove-interest-0')) + + expect(getByTestId('interests-log').textContent).toBe( + JSON.stringify([ + { id: 1, interestName: '' }, + { id: 2, interestName: '' }, + ]), + ) + }) })