react-final-form
react-final-form copied to clipboard
Async field & async record level validation
If the record-level validation is async, it waits for async field-level validation. Is there a way around this?
Say there's a validation error on the email
field. When I type in the username
field, email
s error will be undefined
until username
resolves. This causes all of the shown errors to flicker whenever typing in username
.
This comes from using yup
for record-level validation, and debouncing a field-level validation.
EDIT: Ah, schema.validateSync
fixed the issue, turning record-level into synchronous. Still wondering, though?
function sleep() {
return new Promise((resolve) => { setTimeout(resolve, 1000); });
}
export default function Foo() {
return (
<Form
validate={async (values) => {
const errors = {};
if (!values.email) {
errors.email = 'Required';
}
return errors;
}}
render={() => (
<form>
<Field
name="username"
validate={async () => { await sleep(); }}
render={({ input, meta }) => (
<input {...input} />
)}
/>
<Field
name="email"
render={({ input, meta }) => (
<>
<input {...input} />
{(meta.touch && meta.error) && meta.error}
</>
)}
/>
</form>
)}
/>
);
}
Testable here: https://codesandbox.io/s/loving-ramanujan-b954o9?file=/src/App.tsx:935-956
I realise this is an older issue, but just wanted to note that you can handle this.
Firstly, you can set validateFields
on each <Field>
to an empty array so that changing their value only triggers the validation for the current field, and not other fields.
That fixes the flashing of other async errors, but having an async validate
function for the current field can still cause its error to flash as well. So, to get around this, I've started using a mutated useField
hook that stores the error
and returns it if the field is still validating. E.g.
import { useRef, useMemo } from 'react';
import { UseFieldConfig, useField } from 'react-final-form';
const useMutatedField = (name: string, config?: UseFieldConfig<any>) => {
const { input, meta } = useField(name, config);
const { error, validating, invalid, valid } = meta;
// store previous error state to handle async validation cases
// (where the `error` and `valid`/`invalid` are reset);
// using a ref here and relying on the FieldState for updates
// instead of useState, which would cause extra re-renders
const errorRef = useRef(error);
if (!validating) {
errorRef.current = error;
}
// ensure we only return a new meta object reference
// if the original has also updated;
// shallow-copy because the original prevents values being set
const mutatedMeta = useMemo(() => ({ ...meta }), [meta]);
// now mutate the new object in place so that we do not cause extra re-renders
mutatedMeta.error = error || errorRef.current;
// only set `invalid` and `valid` if they were defined in `meta`,
// (indicating that they were subscribed to)
if (typeof invalid !== 'undefined') {
mutatedMeta.invalid = invalid || Boolean(mutatedMeta.error);
}
if (typeof valid !== 'undefined') {
mutatedMeta.valid = mutatedMeta.error ? false : valid;
}
return { input, meta: mutatedMeta };
};
export default useMutatedField;