表单校验关于前端来说无疑是个繁琐的事情,好在后台办理组件库例如Antd、Element帮咱们处理了这些麻烦。可是关于C端说,UI设计师总会为网站定制特性的表单页面,这不得不需求咱们切图搞起来。既然结构层和表明层需求咱们亲主动手,而表单校验的行为层逻辑总是相似的,有没有一款好用又便利、又接地气又高逼格的库让咱们解放双手呢?唉,还真有,那便是今日引荐的React-Hook-Form,Github上Star有30多k。看它的官方文档真是享受,因为它让我感觉本来组件还能这样封装,逻辑还能这样处理,格式打开了。有种醍醐灌顶的感觉,妈妈,本来我又会了。

解绑UI,先看看好用的React表单校验库

根本用法

咱们来看下它的根本用法

import { useForm } from "react-hook-form";
function Demo() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = handleSubmit((data) => console.log(data))
  return (
    <div className="Demo">
      <form onSubmit={onSubmit}>
        <input {...register("name")} />
        <input {...register("desc", { required: "请输入描绘" })} />
        {errors.desc && <span>{errors.desc.message}</span>}
        <input type="submit" />
      </form>
    </div>
  );
}
export default Demo;

十分便利,只需求将运用register('name')注册一个name的表单项,并将回来值注入到input框中。假如input框是必填项,register办法供给第二个参数,register("desc", { required: "请输入描绘" })。咱们能够从formState.errors字段获取校验的错误信息进行显现。接着handleSubmit是一个高阶函数,可传入回调函数。handleSubmit(data => console.log(data))回来onSubmit函数,当咱们触发onSubmit时可在回调函数中获取表单数据。

register还能够自定义校验规则,例如

register("name", {
  validate: (value) => {
    if (/^[A-Za-z]+$/i.test(value)) {
      return true;
    }
    return "只允许输入英文字母";
  },
})

当然,一些常用的minmaxminLength等校验特点都是能够直接运用。

你可能会猎奇<input {...register("name")} />中register究竟回来了哪些字段给到表单项,咱们应该大约猜到无非便是onChangevalue之类的特点,啀,还不彻底定对。它回来的主要字段有nameonChangeonBlurref,没有value?没有!React Hook Form 注重削减重烘托以达到高性能的目的,采用非受控组件的办法。经过ref拿到inputselecttextarea等原生组件节点来设置value值。

因为不需求特意运用useState来实时存储表单数据,因此咱们输入框输入等操作时,并不会影响组件从头烘托。

解绑UI,先看看好用的React表单校验库

formState监听表单状况

useForm还回来了formState字段,里边有校验错误信息、是否在校验、是否提交等等特点。

const {
  formState: { 
    errors, isDirty, isValidating, isSubmitted
    // ...
  },
} = useForm();

这些特点被开发者运用且改动时,才会触发组件烘托,不运用时不会形成重烘托。什么意思呢?咱们运用errors字段来看下区别。

没有运用errors字段,不会触发重烘托

解绑UI,先看看好用的React表单校验库

运用errors字段,当errors变化时会主动触发重烘托,获取最新的errors数据

解绑UI,先看看好用的React表单校验库

只需求根据咱们是否解构运用来判别,是否需求监听errors变化。第一眼看上去是不是很神奇,很有灵性,不需求开发者操心,就能够防止一些不必要的性能耗费。那它是怎样做到的呢?细心想想咱们怎样监听是否运用了某个字段,当然便是咱们陈词滥调的Object.defineProperty或者Proxy。还真是,源码传送门

大约的思路便是

const initialFormState = {
  isDirty: false,
  isValidating: false,
  errors: {},
  // ...
}
function useForm() {
  const [formState, updateFormState] = useState({ ...initialFormState })
  const _formControl = React.useRef({
    control: {
      _formState: { ...initialFormState },
      _proxyFormState: {}
    }
  });
  // ...
  // 对formState进行代理
  _formControl.current.formState = getProxyFormState(
    formState, 
    _formControl.current.control
  );
  return _formControl.current;
}
function getProxyFormState(formState, control) {
  const result = {};
  for (const key in formState) {
    Object.defineProperty(result, key, {
      get: () => {
        // 只需开发者解构运用了某个字段,即触发了get办法,则设置该字段代理状况为true
        control._proxyFormState[key] = true;
        return formState[key];
      },
    });
  }
  return result
}

useForm只用了一个useState,一般不会去更新state的状况,而是用useRef创建的_formControl.control._formState来存储最新值,这样确保不会触发组件更新。

例如errors字段有变动了,才会更新useState的值

// errors有变化时,且_proxyFormState.errors === true
if (_formControl.control._proxyFormState.errors) {
  // 更新useState中的值,触发重烘托
  updateFormState({ ...control._formState });
}

register回来的ref

处理了咱们的猎奇,接着往下讲。前面提到register("name")回来的一些字段nameonChangeonBlurref等会挂载到表单组件上,那假如咱们本身需求拿到表单组件的ref,或者监听事情怎样办?

function Demo() {
  const inputRef = useRef(null);
  const { register, handleSubmit } = useForm();
  const { ref, onBlur, ...rest } = register("name", {
    required: "请输入称号",
  });
  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input
        onBlur={(e) => {
          onBlur(e);
          // 处理blur事情
          console.log("blur");
        }}
        ref={(e) => {
          ref(e);
          // 拿到输入框ref
          inputRef.current = e;
        }}
        {...rest}
      />
      <input type="submit" />
    </form>
  );
}

那你要说register上述办法不得劲啊,有时候自己封装的表单组件没有供给ref,或者便是不想经过ref来绑定。那也是能够手动setValue

const CustomInput = ({ name, label, value, onChange }) => (
  <>
    <label htmlFor={name}>{label}</label>
    <input name={name} value={value} onChange={onChange} />
  </>
);
function Demo() {
  const {
    register,
    handleSubmit,
    setValue,
    setError,
    watch,
    formState: { errors },
  } = useForm({ defaultValues: { name: '' } });
  const onValidateName = (value) => {
    if (!value) {
      setError("name", { message: "请输入" });
    } else if (!/^[A-Z]/.test(value)) {
      setError("name", { message: "首字符有必要为大写字母" });
    }
  };
  useEffect(() => {
    register("name");
  }, []);
  return (
    <form
      onSubmit={handleSubmit((data) => {
        // 手动增加触发onSubmit时校验
        if (!onValidateName(data.name)) {
          return;
        }
        console.log(`提交:${JSON.stringify(data)}`);
      })}
    >
      <CustomInput
        label="称号"
        name="name"
        value={watch("name")}
        onChange={(value) => {
          // 手动增加触发onChange时校验
          onValidateName(value);
          setValue("name", value);
        }}
      />
      {errors.name && <span>{errors.name.message}</span>}
      <input type="submit" />
    </form>
  );
}

能够看到,假如咱们需求受控组件的办法能够运用value={watch("name")}传递给组件(当然纷歧定需求)。

假如需求输入操作时能够触发校验规则,只能够手动增加了,上面咱们封装了onValidateName校验函数,为了让输入改动提交表单时校验规则一致,所以在onChangehandleSubmit回调函数中都增加了校验。

看起来是麻烦了点,假如一定要受控组件并且纷歧定能供给ref,这个库也为咱们考虑了这种情况,供给了Controller组件给咱们,这样就简洁一点了。

import { Controller, useForm } from "react-hook-form";
const CustomInput = ({ name, label, value, onChange }) => (
  <>
    <label htmlFor={name}>{label}</label>
    <input name="name" value={value} onChange={onChange} />
  </>
);
function Demo() {
  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: {
      name: "",
    },
  });
  return (
    <form
      onSubmit={handleSubmit((data) => {
        console.log(`提交:${JSON.stringify(data)}`);
      })}
    >
      <Controller
        render={({ field }) => <CustomInput label="称号" {...field} />}
        name="name"
        control={control}
        rules={{
          required: {
            value: true,
            message: "请输入",
          },
          pattern: {
            value: /^[A-Z]/,
            message: "首字符有必要为大写字母",
          },
        }}
      />
      {errors.name && <span>{errors.name.message}</span>}
      <input type="submit" />
    </form>
  );
}

仍是回归到最初,假如咱们能供给ref,仍是尽量供给吧。非受控组件至少能削减烘托次数。例如运用forwardRef

const CustomInput = forwardRef(({ name, label, onChange, onBlur }, ref) => (
  <>
    <label htmlFor={name}>{label}</label>
  	<input name={name} onChange={onChange} onBlur={onBlur} ref={ref} />
  </>
));

表单联动

前面提到React-hook-form运用的是非受控组件计划,那假如咱们需求实时获取监听最新的表单值呢?能够如下

function Demo() {
  const { watch } = useForm();
  // 监听单个值
  const name = watch('name');
  // 监听多个值
  const [name2, desc] = watch(['name', 'desc'])
  // 监听所有值
  useEffect(() => {
    const { unsubscribe } = watch((values) => {
      console.log("values", values);
    });
    return () => unsubscribe();
  }, []);
  return (
    <form>
      <input {...register("name")} />
      <input {...register("desc", { required: "请输入描绘" })} />
    </form>
  );
}

这儿运用了观察者模式,只需咱们对需求观察的字段值改动了,才会触发组件烘托。

那么运用watch,咱们能够很简略做到表单联动

export default function Demo() {
  const { register, watch, handleSubmit } = useForm({
    shouldUnregister: true,
  });
  const [data, setData] = useState({});
  return (
    <div className="App">
      <form onSubmit={handleSubmit(setData)}>
        <div>
          <label htmlFor="name">称号:</label>
          <input {...register("name")} />
        </div>
        <div>
          <label htmlFor="more">更多:</label>
          <input type="checkbox" {...register("more")} />
        </div>
        {watch("more") && (
          <div>
            <label>年龄:</label>
            <input type="number" {...register("age")} />
          </div>
        )}
        <input type="submit" />
      </form>
      <div>提交数据:{JSON.stringify(data)}</div>
    </div>
  );
}

解绑UI,先看看好用的React表单校验库

是不是很便利,传入useForm({ shouldUnregister: true });,就能够主动撤销注册不需求的表单项,比如上面的年龄。Reat-hook-form又是咋做到主动撤销注册不要的表单项呢,仍是从ref上。

首要询问下<input ref={e => console.log(e)} />一般从挂载到注销会打印几回?一般两次,第一次打印input的dom节点,另一次打印null

所以React-hook-form也是判别null时撤销注册的,下面也描绘下简略做法

const _names = {
  unMount: new Set()
}
ref(ref) {
  if (ref) {
    register(name, options);
  } else {
    options.shouldUnregister && unMount.add(name);
  }
}

然后在useEffect中撤销注册


useEffect({
  const _removeUnmounted = () => {
    for (const name of _names.unMount) {
      unregister(name)
    }
    _names.unMount = new Set();
  }
  _removeUnmounted()
})

咱们现在了解了根本原理,那前面说不供给ref的组件咋么办,shouldUnregister是不会起作用,那只能手动移除了

export default function Demo() {
  const { register, watch, handleSubmit, unregister, setValue } = useForm();
  const [data, setData] = useState({});
  const more = watch("more");
  useEffect(() => {
    if (more) {
      register("age");
    } else {
      unregister("age");
    }
  }, [more]);
  return (
    <div className="App">
      <form onSubmit={handleSubmit(setData)}>
        <div>
          <label htmlFor="name">称号:</label>
          <input {...register("name")} />
        </div>
        <div>
          <label htmlFor="more">更多:</label>
          <input type="checkbox" {...register("more")} />
        </div>
        {more && (
          <CustomInput
            label="年龄"
            name="age"
            onChange={(value) => setValue("age", value)}
          />
        )}
        <input type="submit" />
      </form>
      <div>提交数据:{JSON.stringify(data)}</div>
    </div>
  );
}

或者运用上面说的库供给的Controller组件

基于React-hook-form封装表单组件

最终,假定咱们开发好了咱们的表单组件,再结合React-hook-form校验库运用,就能够完结咱们网站专属的表单页啦。假如咱们的表单组件在网站或项目中多个地方用到,或许咱们能够再进一层封装。如下运用是不是简介很多。

function Demo() {
  const [data, setData] = useState({});
  return (
    <div className="App">
      <Form onFinish={setData}>
        <FormItem label="称号" name="name" rule={{ required: "请输入称号" }}>
          <CustomInput />
        </FormItem>
        <FormItem label="性别" name="gender" rule={{ required: "请挑选性别" }}>
          <CustomSelect options={["男", "女", "其他"]} />
        </FormItem>
      </Form>
      <div>提交数据:{JSON.stringify(data)}</div>
    </div>
  );
}

咱们现在来动手简略实现一个。首要是咱们自定义开发的表单组件,例如输入框、挑选框等。

const CustomInput = React.forwardRef(({ size = "middle", ...rest }, ref) => (
  <input {...rest} className={`my-input my-input-${size}`} ref={ref} />
));
const CustomSelect = React.forwardRef(
  ({ size = "middle", options, placeholder = "请挑选", ...rest }, ref) => (
    <select {...rest} className={`my-select my-select-${size}`} ref={ref}>
      <option value="">{placeholder}</option>
      {options.map((value) => (
        <option key={value} value={value}>
          {value}
        </option>
      ))}
    </select>
  )
);

紧接着咱们封装Form容器组件

const Form = ({ children, defaultValues, onFinish }) => {
  const {
    handleSubmit,
    register,
    formState: { errors },
  } = useForm({ defaultValues });
  return (
    <form onSubmit={handleSubmit(onFinish)}>
      {React.Children.map(children, (child) =>
        child.props.name
          ? React.cloneElement(child, {
              ...child.props,
              register,
              error: errors[child.props.name],
              key: child.props.name,
            })
          : child
      )}
      <input type="submit" />
    </form>
  );
};

一般Form中的children便是FormItem组件,咱们对其props弥补了register办法和error。

然后咱们再来封装下FormItem组件

const FormItem = ({ children, name, label, register, rule, error }) => {
  // 简略处理:判别FormItem 只能传入一个child
  const child = React.Children.only(children);
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      {React.cloneElement(child, {
        ...child.props,
        ...register(name, rule),
        name,
      })}
      {error && <span>{error.message}</span>}
    </div>
  );
};

FormItem组件的children一般便是输入框、挑选框等,咱们调用register办法将回来的ref、onChange等特点再弥补到输入框、挑选框等表单组件上。

至此,咱们自行封装的表单组件库demo版就完结啦。那其实咱们还有很多容错判别、更多功能还没有处理,能够渐渐增加。例如咱们需求有重置表单的功能

function Demo() {
  const [data, setData] = useState({});
  const formRef = useRef();
  return (
    <div className="App">
      <Form onFinish={setData} ref={formRef}>
        <FormItem label="称号" name="name" rule={{ required: "请输入称号" }}>
          <CustomInput />
        </FormItem>
      </Form>
      <div onClick={() => formRef.current.reset()}>
        重置
      </div>
    </div>
  );
}

那么咱们的Form组件就需求把useForm回来的办法等暴露出去

const Form = React.forwardRef(({ children, defaultValues, onFinish }, ref) => {
  const form = useForm({ defaultValues });
  const {
    handleSubmit,
    register,
    formState: { errors },
  } = form;
  React.useImperativeHandle(ref, () => form);
  return (
    <form onSubmit={handleSubmit(onFinish)}>
      {React.Children.map(children, (child) =>
        child.props.name
          ? React.cloneElement(child, {
              ...child.props,
              register,
              error: errors[child.props.name],
              key: child.props.name,
            })
          : child
      )}
      <input type="submit" />
    </form>
  );
});

好啦太多需求弥补了,就纷歧一讲述。

最终

经过学习运用React-hook-form,给开发节约不少时刻,也get到了不少技巧,收获满满的,也期望对你有用。