From bce9b2c084abb5dc1ce0793809d820b38676a61d Mon Sep 17 00:00:00 2001 From: MVaik <105720462+MVaik@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:56:06 +0200 Subject: [PATCH 1/2] perf(react-form): skip updating isValidating by default BREAKING CHANGE: isValidating is now opt-in --- packages/form-core/src/FieldApi.ts | 18 ++++- packages/react-form/tests/useField.test.tsx | 90 +++++++++++++++++---- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 3013e7aaa..c201f80c9 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -439,6 +439,10 @@ export interface FieldOptions< * Disable the `flat(1)` operation on `field.errors`. This is useful if you want to keep the error structure as is. Not suggested for most use-cases. */ disableErrorFlat?: boolean + /** + * Should the field's isValidating state be updated during validation + */ + trackValidationState?: boolean } /** @@ -1473,12 +1477,14 @@ export class FieldApi< >, ) - if (!this.state.meta.isValidating) { + if (!this.state.meta.isValidating && this.options.trackValidationState) { this.setMeta((prev) => ({ ...prev, isValidating: true })) } for (const linkedField of linkedFields) { - linkedField.setMeta((prev) => ({ ...prev, isValidating: true })) + if (linkedField.options.trackValidationState) { + linkedField.setMeta((prev) => ({ ...prev, isValidating: true })) + } } /** @@ -1577,10 +1583,14 @@ export class FieldApi< await Promise.all(linkedPromises) } - this.setMeta((prev) => ({ ...prev, isValidating: false })) + if (this.options.trackValidationState) { + this.setMeta((prev) => ({ ...prev, isValidating: false })) + } for (const linkedField of linkedFields) { - linkedField.setMeta((prev) => ({ ...prev, isValidating: false })) + if (linkedField.options.trackValidationState) { + linkedField.setMeta((prev) => ({ ...prev, isValidating: false })) + } } return results.filter(Boolean) diff --git a/packages/react-form/tests/useField.test.tsx b/packages/react-form/tests/useField.test.tsx index a5969c4e8..96e53e107 100644 --- a/packages/react-form/tests/useField.test.tsx +++ b/packages/react-form/tests/useField.test.tsx @@ -290,28 +290,26 @@ describe('useField', () => { ) } - - return ( - - {({ handleChange, state }) => ( - - )} - - ) + return null }} + + {({ handleChange, state }) => ( + + )} + ) } - const { getByTestId } = render() + const { getByTestId, debug } = render() const showFirstFieldInput = getByTestId('show-first-field') @@ -1130,4 +1128,64 @@ describe('useField', () => { // field2 should not have rerendered expect(renderCount.field2).toBe(field2InitialRender) }) + + it('should render once per change if not tracking validation state', async () => { + let regularRenders = 0 + let rendersWithTracking = 0 + const inputText = 'example' + const expectedRenders = inputText.length + // 1 by default and seems like 1 for useStore + const baseRenders = 2 + + function Comp() { + const form = useForm({ + defaultValues: { + field1: '', + field2: '', + }, + }) + + return ( + <> + { + regularRenders++ + return ( + field.handleChange(e.target.value)} + /> + ) + }} + /> + { + rendersWithTracking++ + return ( + field.handleChange(e.target.value)} + /> + ) + }} + /> + + ) + } + + const { getByTestId } = render() + await user.type(getByTestId('fieldinput1'), inputText) + expect(regularRenders).toEqual(expectedRenders + baseRenders) + + await user.type(getByTestId('fieldinput2'), inputText) + // Each change triggers two renders due to isValidating state being updated + expect(rendersWithTracking).toEqual(expectedRenders * 2 + baseRenders) + }) }) From 1765bff20630d604b2387ea356b4c66f1032b409 Mon Sep 17 00:00:00 2001 From: MVaik <105720462+MVaik@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:20:26 +0200 Subject: [PATCH 2/2] chore(react-form): revert unprompted test "fix" --- packages/react-form/tests/useField.test.tsx | 29 +++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/react-form/tests/useField.test.tsx b/packages/react-form/tests/useField.test.tsx index 96e53e107..015652e2b 100644 --- a/packages/react-form/tests/useField.test.tsx +++ b/packages/react-form/tests/useField.test.tsx @@ -290,26 +290,27 @@ describe('useField', () => { ) } - return null + return ( + + {({ handleChange, state }) => ( + + )} + + ) }} - - {({ handleChange, state }) => ( - - )} - ) } - const { getByTestId, debug } = render() + const { getByTestId } = render() const showFirstFieldInput = getByTestId('show-first-field')