From b2996319696014540d5ce90d050c929934c08b46 Mon Sep 17 00:00:00 2001 From: anguiao Date: Sun, 24 May 2026 16:21:54 +0800 Subject: [PATCH] fix: keep vue field slot state reactive --- .changeset/vue-form-field-slot-state.md | 5 ++ packages/vue-form/src/useField.tsx | 7 +- packages/vue-form/tests/useField.test.tsx | 78 +++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 .changeset/vue-form-field-slot-state.md diff --git a/.changeset/vue-form-field-slot-state.md b/.changeset/vue-form-field-slot-state.md new file mode 100644 index 000000000..9b2c109f0 --- /dev/null +++ b/.changeset/vue-form-field-slot-state.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-form': patch +--- + +Fix `Field` scoped slot `state` reactivity after field value and meta updates. diff --git a/packages/vue-form/src/useField.tsx b/packages/vue-form/src/useField.tsx index e51d79681..599154839 100644 --- a/packages/vue-form/src/useField.tsx +++ b/packages/vue-form/src/useField.tsx @@ -375,7 +375,12 @@ export function useField< }, ) - return { api: extendedFieldApi.value, state: fieldState.value } as const + return { + api: extendedFieldApi.value, + get state() { + return fieldState.value + }, + } as const } export type FieldComponentProps< diff --git a/packages/vue-form/tests/useField.test.tsx b/packages/vue-form/tests/useField.test.tsx index f01707cc3..90bfffb90 100644 --- a/packages/vue-form/tests/useField.test.tsx +++ b/packages/vue-form/tests/useField.test.tsx @@ -225,6 +225,84 @@ describe('useField', () => { expect(getByText(error)).toBeInTheDocument() }) + it('should keep field slot state reactive', async () => { + type Person = { + firstName: string + } + + const serverError = 'First name is already taken' + + const Comp = defineComponent(() => { + const form = useForm({ + defaultValues: { + firstName: '', + } as Person, + }) + + function setServerError() { + form.setFieldMeta('firstName', (meta) => ({ + ...meta, + errorMap: { + ...meta.errorMap, + onServer: serverError, + }, + errorSourceMap: { + ...meta.errorSourceMap, + onServer: 'form', + }, + })) + } + + return () => ( +
+ + + + {({ + field, + state, + }: { + field: AnyFieldApi + state: AnyFieldApi['state'] + }) => ( +
+ + field.handleChange((e.target as HTMLInputElement).value) + } + /> +

{state.value}

+

{state.meta.errorMap.onServer}

+
+ )} +
+
+ ) + }) + + const { getByTestId, getByText } = render(Comp) + const input = getByTestId('fieldinput') + + await user.type(input, 'Grace') + await waitFor(() => + expect(getByTestId('slotvalue')).toHaveTextContent('Grace'), + ) + + await user.click(getByText('Reset')) + await waitFor(() => expect(input).toHaveValue('Ada')) + expect(getByTestId('slotvalue')).toHaveTextContent('Ada') + + await user.click(getByText('Set server error')) + await waitFor(() => + expect(getByTestId('sloterror')).toHaveTextContent(serverError), + ) + }) + it('should handle arrays with subvalues', async () => { const fn = vi.fn()