-
-
Notifications
You must be signed in to change notification settings - Fork 443
Zod's async refine
/ superRefine
called twice when used with TanStack Form's onSubmitAsync
validator
#1431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
A note about the workaround: You can use the onSubmitAsync: async ({ value, formApi }) => { // <--- Workaround implementation
console.log('Manually triggering safeParseAsync');
// This validation triggers the network request via checkUser
return await formApi.parseValuesWithSchemaAsync(asyncIgnoredUsersSchemaRefine);
} Edit This appears to also be an issue of it('should only run onSubmitAsync schemas once on submit', async () => {
vi.useFakeTimers()
const asyncCallMock = vi.fn()
const schema = z.object({
name: z.string().refine(async () => {
asyncCallMock()
return true
}, 'Some error message'),
})
const form = new FormApi({
defaultValues: {
name: '',
},
validators: {
onSubmitAsync: schema,
},
})
form.mount()
form.handleSubmit()
await vi.runAllTimersAsync()
expect(asyncCallMock).toHaveBeenCalledOnce()
}) |
I have also tried with that function, but it seems that it also happens. Maybe the problem is related? I did my own helper function: const prefixSchemaToErrors = (issues: readonly ZodIssue[]) => {
const schema = new Map<string, ZodIssue[]>()
for (const issue of issues) {
const path = [...issue.path]
.map(segment => (typeof segment === 'number' ? `[${segment}]` : segment))
.join('.')
.replace(/\.\[/g, '[')
schema.set(path, (schema.get(path) ?? []).concat(issue))
}
return Object.fromEntries(schema)
}
export const checkSchemaOnSubmitAsync =
<TFormData>(schema: ZodSchema<TFormData>): FormValidateAsyncFn<TFormData> =>
async ({ value }) => {
const validation = await schema.safeParseAsync(value)
if (!validation.success) {
const schemaErrors = prefixSchemaToErrors(validation.error.issues)
return {
form: schemaErrors,
fields: schemaErrors
}
}
} |
Interesting ... that should narrow it down quite a bit. |
Complete Workaround ImplementationFor anyone experiencing the double execution of async 1. Helper Functions & Factories: import { z, ZodIssue, ZodSchema } from 'zod';
/**
* Formats Zod issues into an object mapping dot-notation paths to issue arrays.
* Useful for structuring errors for TanStack Form (may need adjustment based on version).
*/
const prefixSchemaToErrors = (issues: readonly ZodIssue[]) => {
const schema = new Map<string, ZodIssue[]>();
for (const issue of issues) {
const path = [...issue.path]
.map(segment => (typeof segment === 'number' ? `[${segment}]` : segment))
.join('.')
.replace(/\.\[/g, '[');
schema.set(path, (schema.get(path) ?? []).concat(issue));
}
return Object.fromEntries(schema);
};
/**
* Core async validator logic. Parses the value against the schema.
* Returns Zod issues for field validation failure or a specific
* error object structure for form validation failure.
*/
const checkSchemaOnValidatorAsync = async <TData>({
schema,
value,
validationSource = 'field'
}: {
schema: ZodSchema<TData>;
value: TData;
validationSource?: 'field' | 'form';
}) => {
const validation = await schema.safeParseAsync(value);
if (!validation.success) {
// For field-level, return raw issues
if (validationSource === 'field') {
return validation.error.issues;
}
// For form-level, return the structured errors object
const schemaErrors = prefixSchemaToErrors(validation.error.issues);
return {
form: schemaErrors,
field: schemaErrors
};
}
// Return undefined on success
return undefined;
};
/**
* Factory function to create an async validator suitable for TanStack Form FIELDS
* (e.g., field-level onChangeAsync, onBlurAsync, onSubmitAsync).
*/
export const checkSchemaOnFieldValidatorAsync =
<TData>(schema: ZodSchema<TData>) =>
// Note: TanStack field validators often receive { value, signal, fieldApi }.
// This simplified version only uses value. Adapt if signal/fieldApi are needed.
({ value }: { value: TData }) =>
checkSchemaOnValidatorAsync({ schema, value, validationSource: 'field' });
/**
* Factory function to create an async validator suitable for TanStack Form's
* form-level async validators.
*/
export const checkSchemaOnFormValidatorAsync =
<TFormData>(schema: ZodSchema<TFormData>): FormValidateAsyncFn<TFormData> =>
({ value }) => // Assumes FormValidateAsyncFn provides { value }
checkSchemaOnValidatorAsync({ schema, value, validationSource: 'form' }); 2. Usage Examples: <form.AppField
name='username' // The name of the field in your form data
validators={{
// Use the factory for async validation on the field
onSubmitAsync: checkSchemaOnFieldValidatorAsync(checkUserSchema)
}}
children={field => (
// Your field rendering logic...
)}
/> B. Form-Level const form = useAppForm({
defaultValues: {
username: '',
note: ''
},
validators: {
// Use the factory for the form-level onSubmitAsync
onSubmitAsync: checkSchemaOnFormValidatorAsync(annotatedUsersSchema) // <<--- Usage
},
onSubmit: ({ value, formApi }) => {
// This runs only if sync and async validations pass
...
}); |
Describe the bug
When using a Zod schema with an asynchronous
refine
orsuperRefine
validation directly within TanStack Form'sonSubmitAsync
validator option, the asynchronous function (which performs a network request in this case) appears to be executed twice in first submission attempt. This results in duplicate network request being visible in the browser's Network tab. The subsequent submission attempts runs only once.Your minimal, reproducible example
https://codesandbox.io/p/sandbox/serene-brook-gmt73s
Steps to reproduce
Define a Zod schema with async validation that performs a network request:
Using
refine
:Using
superRefine
:Use the schema in
useAppForm
(oruseForm
)onSubmitAsync
:Trigger form submission: Open the browser's Network tab, fill the
ignoredUser
field, and submit the form.Expected behavior
The asynchronous validation logic within
refine
orsuperRefine
should execute only once. Consequently, the network request initiated by thecheckUser
function should appear only once in the Network tab per submission attempt.How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
TanStack Form adapter
None
TanStack Form version
1.6.3
TypeScript version
5.8.3
Additional context
Actual Behavior:
The network request initiated by the
checkUser
function (triggered via the asyncrefine
orsuperRefine
) appears twice in the browser's Network tab for a single form submission. The console logs (if added) might also appear twice, confirming the duplicate execution.Workaround:
A workaround involves manually calling Zod's
safeParseAsync
method within theonSubmitAsync
function body, instead of passing the schema directly. This prevents the double execution and thus the duplicate network request.(Note: The error formatting in the workaround might need adjustment based on the specific TanStack Form version).
Possible Cause:
This seems related to how TanStack Form internally handles or invokes Zod schemas provided to onSubmitAsync. It might be triggering the schema's asynchronous validation process more than once, leading to the unintended duplicate side effect (the network request).
The text was updated successfully, but these errors were encountered: