[Translated] How to create a less than 100 lines of code with asynchronous form validation library React Hooks

Form validation is a very tricky thing. After in-depth understanding to achieve a form, you will find a large number of boundary scene to deal with. Fortunately, there is a lot form validation library, which provides the necessary metering (Annotation: as dirty, invalid, inItialized, pristine, and so on) and handlers, to make us realize a robust form. But I want to use React Hooks API to build a 100 lines of code following form validation library to challenge themselves. Although React Hooks still in the experimental stage, but this is a proof Hooks achieve form validation of React.

I must declare that I wrote this library is indeed less than 100 lines of code. But this tutorial, there are about 200 lines of code, because I need to explain clearly how the library is used.

I have seen most of the newbie tutorial form library are inseparable from the three core topics: asynchronous verification , form linkage: Some form items need to check in when other form item to change the trigger, form validation efficiency optimization. I am very disgusted with those who use the tutorial scene fixed, while ignoring the impact of the practice of other variables. Because often counterproductive in the real scene, so my tutorial will try to cover more real scene.

Our goal needs to be met:

  • Synchronization Check single table item, when the value table including item changes, the change will follow the form items are dependent

  • Checking asynchronous single table item, when the value table including item changes, the change will follow the form items are dependent

  • Before submitting the form, check all the tables in the individual synchronization

  • Before submitting the form, check all asynchronous form items

  • Asynchronous try to submit, if the form is submitted fails to show an error message is returned

  • Provide verification forms to the developer function, so that developers can at the right time, such as when the check form onBlur

  • Allows multiple calibration of a single table item

  • When the ban submit form validation does not pass

  • Form error message only to show up only when there is an error message or attempt to change the form is submitted

We will achieve through a registration form contains the account user name, password, password secondary confirmation to cover these scenarios. Here is a simple interface, we come together to build this library bar.

const form = useForm({
  onSubmit,
});

const usernameField = useField("username", form, {
  defaultValue: "",
  validations: [
    async formData => {
      await timeout(2000);
      return formData.username.length < 6 && "Username already exists";
    }
  ],
  fieldsToValidateOnChange: []
});
const passwordField = useField("password", form, {
  defaultValue: "",
  validations: [
    formData =>
      formData.password.length < 6 && "Password must be at least 6 characters"
  ],
  fieldsToValidateOnChange: ["password", "confirmPassword"]
});
const confirmPasswordField = useField("confirmPassword", form, {
  defaultValue: "",
  validations: [
    formData =>
      formData.password !== formData.confirmPassword &&
      "Passwords do not match"
  ],
  fieldsToValidateOnChange: ["password", "confirmPassword"]
});

// const { onSubmit, getFormData, addField, isValid, validateFields, submitted, submitting } = form
// const { name, value, onChange, errors, setErrors, pristine, validate, validating } = usernameField
复制代码

This is a very simple API, but really gave us a lot of flexibility. You may have realized, this interface contains two names like functions, validation and validate. validation is defined as a function with the form data and individual table name as a parameter, if the verification problem, an error message is returned, at the same time it will return a dummy value (Translator's Note: can be converted to false value). On the other hand, validate all validation function to perform this function form item, and update the list of errors form item.

Most important, we need a framework to deal with changes in the form and form values ​​submitted. Our first attempt will not contain any check, it is only used to process the form of the state.

// 跳过样板代码: imports, ReactDOM, 等等.

export const useField = (name, form, { defaultValue } = {}) => {
  let [value, setValue] = useState(defaultValue);

  let field = {
    name,
    value,
    onChange: e => {
      setValue(e.target.value);
    }
  };
  // 注册表单项
  form.addField(field);
  return field;
};

export const useForm = ({ onSubmit }) => {
  let fields = [];

  const getFormData = () => {
    // 获得一个包含原始表单数据的 object
    return fields.reduce((formData, field) => {
      formData[field.name] = field.value;
      return formData;
    }, {});
  };

  return {
    onSubmit: async e => {
      e.preventDefault(); // 阻止默认表单提交
      return onSubmit(getFormData());
    },
    addField: field => fields.push(field),
    getFormData
  };
};

const Field = ({ label, name, value, onChange, ...other }) => {
  return (
    <FormControl className="field">
      <InputLabel htmlFor={name}>{label}</InputLabel>
      <Input value={value} onChange={onChange} {...other} />
    </FormControl>
  );
};

const App = props => {
  const form = useForm({
    onSubmit: async formData => {
      window.alert("Account created!");
    }
  });

  const usernameField = useField("username", form, {
    defaultValue: ""
  });
  const passwordField = useField("password", form, {
    defaultValue: ""
  });
  const confirmPasswordField = useField("confirmPassword", form, {
    defaultValue: ""
  });

  return (
    <div id="form-container">
      <form onSubmit={form.onSubmit}>
        <Field {...usernameField} label="Username" />
        <Field {...passwordField} label="Password" type="password" />
        <Field {...confirmPasswordField} label="Confirm Password" type="password" />
        <Button type="submit">Submit</Button>
      </form>
    </div>
  );
};
复制代码

There is no code is too difficult to understand. The value of the form is our only concern. Each form item before it is initialized to the end of their registration on the form. Our onChange function is also very simple. Here is the most complex function is getFormData, even so, it can not be compared with the abstract syntax reduce. All table getFormData traverse, and returns a plain object to represent the values ​​of the form. Finally, it is worth mentioning that at the time of form submission, we need to call preventDefault to prevent the page reload.

Things are going well, and now we have to add it to verify. When the value of the form item is changed or the submission form, we are not what specific form items need to be verified indicated, but check all the form items.

export const useField = (
  name,
  form,
  { defaultValue, validations = [] } = {}
) => {
  let [value, setValue] = useState(defaultValue);
  let [errors, setErrors] = useState([]);

  const validate = async () => {
    let formData = form.getFormData();
    let errorMessages = await Promise.all(
      validations.map(validation => validation(formData, name))
    );
    errorMessages = errorMessages.filter(errorMsg => !!errorMsg);
    setErrors(errorMessages);
    let fieldValid = errorMessages.length === 0;
    return fieldValid;
  };

  useEffect(
    () => {
      form.validateFields(); // 当 value 变化的时候校验表单项
    },
    [value]
  );

  let field = {
    name,
    value,
    errors,
    validate,
    setErrors,
    onChange: e => {
      setValue(e.target.value);
    }
  };
  // 注册表单项
  form.addField(field);
  return field;
};

export const useForm = ({ onSubmit }) => {
  let fields = [];

  const getFormData = () => {
    // 获得一个 object 包含原始表单数据
    return fields.reduce((formData, field) => {
      formData[field.name] = field.value;
      return formData;
    }, {});
  };

  const validateFields = async () => {
    let fieldsToValidate = fields;
    let fieldsValid = await Promise.all(
      fieldsToValidate.map(field => field.validate())
    );
    let formValid = fieldsValid.every(isValid => isValid === true);
    return formValid;
  };

  return {
    onSubmit: async e => {
      e.preventDefault(); // 阻止表单提交默认事件
      let formValid = await validateFields();
      return onSubmit(getFormData(), formValid);
    },
    addField: field => fields.push(field),
    getFormData,
    validateFields
  };
};

const Field = ({
  label,
  name,
  value,
  onChange,
  errors,
  setErrors,
  validate,
  ...other
}) => {
  let showErrors = !!errors.length;
  return (
    <FormControl className="field" error={showErrors}>
      <InputLabel htmlFor={name}>{label}</InputLabel>
      <Input
        id={name}
        value={value}
        onChange={onChange}
        onBlur={validate}
        {...other}
      />
      <FormHelperText component="div">
        {showErrors &&
          errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}
      </FormHelperText>
    </FormControl>
  );
};

const App = props => {
  const form = useForm({
    onSubmit: async formData => {
      window.alert("Account created!");
    }
  });

  const usernameField = useField("username", form, {
    defaultValue: "",
    validations: [
      async formData => {
        await timeout(2000);
        return formData.username.length < 6 && "Username already exists";
      }
    ]
  });
  const passwordField = useField("password", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password.length < 6 && "Password must be at least 6 characters"
    ]
  });
  const confirmPasswordField = useField("confirmPassword", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password !== formData.confirmPassword &&
        "Passwords do not match"
    ]
  });

  return (
    <div id="form-container">
      <form onSubmit={form.onSubmit}>
        <Field {...usernameField} label="Username" />
        <Field {...passwordField} label="Password" type="password" />
        <Field {...confirmPasswordField} label="Confirm Password" type="password" />
        <Button type="submit">Submit</Button>
      </form>
    </div>
  );
};

复制代码

The above code is an improved version, the general look seems to run up, but to be delivered to the user is not enough. This version lost a lot of error messages for the hidden mark state (Translator's Note: flag), these error messages may appear at the wrong time. For example, the user has not finished modifying the input information when the form is immediately verify and display the appropriate error message.

The most basic, we need some basic markup state to inform the UI, if the user does not modify the value of the form item, then do not show an error message. Still further, in addition to these foundations, we also need some additional markup state.

We need a flag state have tried to record the user submit the form and a mark to record the state of the form being submitted or asynchronous form items being check. You may also want to find out why we have to call validateFields inside useEffect, rather than calling in onChange in. We need useEffect because setValue occurs asynchronously, It does not return a promise, we will not provide a callback. Therefore, the only way to completion of setValue allow us to determine, by useEffect is to monitor changes in value.

Now we work together to achieve these so-called mark-state bar. Use them to better improve the UI and details.

export const useField = (
  name,
  form,
  { defaultValue, validations = [], fieldsToValidateOnChange = [name] } = {}
) => {
  let [value, setValue] = useState(defaultValue);
  let [errors, setErrors] = useState([]);
  let [pristine, setPristine] = useState(true);
  let [validating, setValidating] = useState(false);
  let validateCounter = useRef(0);

  const validate = async () => {
    let validateIteration = ++validateCounter.current;
    setValidating(true);
    let formData = form.getFormData();
    let errorMessages = await Promise.all(
      validations.map(validation => validation(formData, name))
    );
    errorMessages = errorMessages.filter(errorMsg => !!errorMsg);
    if (validateIteration === validateCounter.current) {
      // 最近一次调用
      setErrors(errorMessages);
      setValidating(false);
    }
    let fieldValid = errorMessages.length === 0;
    return fieldValid;
  };

  useEffect(
    () => {
      if (pristine) return; // 避免渲染完成后的第一次校验
      form.validateFields(fieldsToValidateOnChange);
    },
    [value]
  );

  let field = {
    name,
    value,
    errors,
    setErrors,
    pristine,
    onChange: e => {
      if (pristine) {
        setPristine(false);
      }
      setValue(e.target.value);
    },
    validate,
    validating
  };
  form.addField(field);
  return field;
};

export const useForm = ({ onSubmit }) => {
  let [submitted, setSubmitted] = useState(false);
  let [submitting, setSubmitting] = useState(false);
  let fields = [];

  const validateFields = async fieldNames => {
    let fieldsToValidate;
    if (fieldNames instanceof Array) {
      fieldsToValidate = fields.filter(field =>
        fieldNames.includes(field.name)
      );
    } else {
      // 如果 fieldNames 缺省,则验证所有表单项
      fieldsToValidate = fields;
    }
    let fieldsValid = await Promise.all(
      fieldsToValidate.map(field => field.validate())
    );
    let formValid = fieldsValid.every(isValid => isValid === true);
    return formValid;
  };

  const getFormData = () => {
    return fields.reduce((formData, f) => {
      formData[f.name] = f.value;
      return formData;
    }, {});
  };

  return {
    onSubmit: async e => {
      e.preventDefault();
      setSubmitting(true);
      setSubmitted(true); // 用户已经至少提交过一次表单
      let formValid = await validateFields();
      let returnVal = await onSubmit(getFormData(), formValid);
      setSubmitting(false);
      return returnVal;
    },
    isValid: () => fields.every(f => f.errors.length === 0),
    addField: field => fields.push(field),
    getFormData,
    validateFields,
    submitted,
    submitting
  };
};

const Field = ({
  label,
  name,
  value,
  onChange,
  errors,
  setErrors,
  pristine,
  validating,
  validate,
  formSubmitted,
  ...other
}) => {
  let showErrors = (!pristine || formSubmitted) && !!errors.length;
  return (
    <FormControl className="field" error={showErrors}>
      <InputLabel htmlFor={name}>{label}</InputLabel>
      <Input
        id={name}
        value={value}
        onChange={onChange}
        onBlur={() => !pristine && validate()}
        endAdornment={
          <InputAdornment position="end">
            {validating && <LoadingIcon className="rotate" />}
          </InputAdornment>
        }
        {...other}
      />
      <FormHelperText component="div">
        {showErrors &&
          errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}
      </FormHelperText>
    </FormControl>
  );
};

const App = props => {
  const form = useForm({
    onSubmit: async (formData, valid) => {
      if (!valid) return;
      await timeout(2000); // 模拟网络延迟
      if (formData.username.length < 10) {
        //模拟服务端返回 400 
        usernameField.setErrors(["Make a longer username"]);
      } else {
        //模拟服务端返回 201 
        window.alert(
          `form valid: ${valid}, form data: ${JSON.stringify(formData)}`
        );
      }
    }
  });

  const usernameField = useField("username", form, {
    defaultValue: "",
    validations: [
      async formData => {
        await timeout(2000);
        return formData.username.length < 6 && "Username already exists";
      }
    ],
    fieldsToValidateOnChange: []
  });
  const passwordField = useField("password", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password.length < 6 && "Password must be at least 6 characters"
    ],
    fieldsToValidateOnChange: ["password", "confirmPassword"]
  });
  const confirmPasswordField = useField("confirmPassword", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password !== formData.confirmPassword &&
        "Passwords do not match"
    ],
    fieldsToValidateOnChange: ["password", "confirmPassword"]
  });

  let requiredFields = [usernameField, passwordField, confirmPasswordField];

  return (
    <div id="form-container">
      <form onSubmit={form.onSubmit}>
        <Field
          {...usernameField}
          formSubmitted={form.submitted}
          label="Username"
        />
        <Field
          {...passwordField}
          formSubmitted={form.submitted}
          label="Password"
          type="password"
        />
        <Field
          {...confirmPasswordField}
          formSubmitted={form.submitted}
          label="Confirm Password"
          type="password"
        />
        <Button
          type="submit"
          disabled={
            !form.isValid() ||
            form.submitting ||
            requiredFields.some(f => f.pristine)
          }
        >
          {form.submitting ? "Submitting" : "Submit"}
        </Button>
      </form>
    </div>
  );
};
复制代码

The last attempt, we added a lot of things inside. It comprises four marks state: pristine, validating, submitted and submitting. Also added fieldsToValidateOnChange, pass it validateFields to declare when the form of value changes of form items which need to be verified. We in the UI layer to control when to show error messages and animations, and disable the submit button to load these markers by the state.

You may have noticed a very special thing validateCounter. We need to record the number of calls validate function because validate before the current call is completed, it is likely to be called again. If this is the scenario, then we should give up the results of the current call, and the call using only the result of individual error status to update the table of the latest time.

After everything is ready, this is our fruit.

React Hooks provides a concise form validation solution. This is what I use this API first attempt. Although a little flawed, but I still feel it's powerful. This interface is a little strange, because it is in accordance with the way I like to come. In addition to these flaws, however, its function is very powerful.

I think also missing some features, such as a callback mechanism to indicate when useState update state is completed, which is a hook that checks the prop changes in the method in useEffect.

postscript

In order to ensure easy to use this tutorial, I deliberately omitted checksum error and exception handling some of the parameters. For example, I did not check incoming form parameter is really a form object. If I can definitely check its type and throws a detailed exception will be better. In fact, I have written the code like this error.

Cannot read property ‘addField’ of undefined
复制代码

Before the release of this code into npm package, but also the appropriate parameters checksum error and exception handling. As I said, if you want to understand, I have used superstruct achieved a check parameter contains the more robust version .

If you find there is a translation error or other areas for improvement, welcome to Denver translation program to be modified and translations PR, also obtained the corresponding bonus points. The beginning of the article Permalink article is the MarkDown the links in this article on GitHub.


Nuggets Translation Project is a high-quality translation of technical articles Internet community, Source for the Nuggets English Share article on. Content covering Android , iOS , front-end , back-end , block chain , product , design , artificial intelligence field, etc., you want to see more high-quality translations, please continue to focus Nuggets translation program , the official micro-blog , we know almost columns .

Guess you like

Origin blog.csdn.net/weixin_34220179/article/details/91393714
Recommended