react-final-form icon indicating copy to clipboard operation
react-final-form copied to clipboard

Async field & async record level validation

Open onosendi opened this issue 3 years ago • 1 comments

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, emails 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>
      )}
    />
  );
}

onosendi avatar Jan 10 '22 21:01 onosendi

Testable here: https://codesandbox.io/s/loving-ramanujan-b954o9?file=/src/App.tsx:935-956

bertho-zero avatar Feb 22 '22 18:02 bertho-zero

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;

mynamesleon avatar Sep 22 '23 22:09 mynamesleon