$ ls ~yifei/notes/

React 中的表单

Posted on:

Last modified:

表单元素和所有其他元素的不同在于:表单元素是输入组件,是有内部状态的,所有其他元素都是 输出元素。原生 HTML 表单在 React 中也能用,但一般情况下,我们会交给一个函数来处理提交表单。 由这个函数负责验证表单等操作,这时我们可以用"受控组件"。

使用受控组件

受控组件中,数据不保存在 DOM 中,而是始终在 JS 中,随时通过 value/onChange 读取验证。 非受控组件数据依然保存在 DOM 树中,只在 submit 时候读取。

function MyForm(props) {
  value, setValue = useState("");

  function handleChange(e) {
    setValue(e.target.value)
  }

  function handleSubmit(e) {
    // 直接读取 js 中保存的 state.value
    console.log("A name was submitted: " + value);
    axios.post("/api/myresource", {value});
    e.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" value={value} onChange={onChange} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

使用原生表单

也可以使用原生组件,而不要直接使用受控组件。

function form2kv(form) {
  const fd = new FormData(form);
  const ret = {};
  for (let key of fd.keys()) {
    ret[key] = fd.get(key);
  }
  return ret;
}

function MyForm(props) {

  // 也可以直接通过读取 events.target 的值来获取
  function handleSubmit1(e) {
    e.preventDefault();
    console.log(e.target.elements.username.value) // from elements property
    console.log(e.target.username.value)          // or directly
  }

  function handleSubmit2(e) {
    e.preventDefault();
    const data = form2kv(e.target);
    axios.post("/api/myresource", data);
  }

  return (
    <form onSubmit={handleSubmit2}>
      <label htmlFor="username">Enter username</label>
      <input type="text" id="username" name="username">
      <label htmlFor="password">Enter password</label>
      <input type="password" id="password" name="password">
      <label htmlFor="birthdate">Enter your birth date</label>
      <input type="date" id="birthdate" name="birthdate">
      <button type="submit">Send data!</button>
    </form>
  )
}

在上面的例子中,我们使用了浏览器自带的 Formdata 对象,通过传入 DOM 中的 Form 元素来 读取其中的数据。另外需要注意一点,我们使用了 onSubmit 事件,而不是 Submit 按钮的 onClick 事件,这样保留了浏览器的原生激活方式,也就是可以使用回车键提交。

处理数据

如果当我们需要对数据做一些后处理时呢?比如:

  • 用户输入的数据是 MM/DD/YYYY, 服务端要求是 YYYY-MM-DD
  • 用户名应该全部大写

这时候可以使用另一个小技巧,data-x 属性。

const inputParsers = {
  date(input) {
    const [month, day, year] = input.split('/');
    return `${year}-${month}-${day}`;
  },
  uppercase(input) {
    return input.toUpperCase();
  },
  number(input) {
    return parseFloat(input);
  },
};

function form2kv(form, parsers = {}) {
  const fd = new FormData(form);
  const ret = {};
  for (let name of data.keys()) {
    const input = form.elements[name];
    const parseName = input.dataset.parse;

    if (parseName) {
      const parser = inputParsers[parseName];
      const parsedValue = parser(data.get(name));
      ret[name] = parsedValue;
    } else {
      ret[name] = data.get(name);
    }
  }
  return ret;
}

function MyForm(props) {
  function handleSubmit(e) {
    e.preventDefault();
    const data = form2kv(e.target);
    fetch("api/myresource", method="POST", body=JSON.stringify(data));
  }
  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Enter username</label>
      <input type="text" id="username" name="username" data-parse="uppercase">
      <label htmlFor="password">Enter password</label>
      <input type="password" id="password" name="password">
      <label htmlFor="birthdate">Enter your birth date</label>
      <input type="date" id="birthdate" name="birthdate" data-parse="date">
      <button type="submit">Send data!</button>
    </form>
  )
}

在上面的例子中,我们使用 data-x 属性指定了应该使用的处理函数。通过表单的 elements 属性, 我们按照 input 的 name 访问元素。而节点的 data-x 属性可以在 dataset[x] 属性中访问。

验证表单

HTML 的原生 API 就支持了很多的验证属性,其中最最好用的就是 pattern 属性了,他可以指定一个 正则来验证表单。而且我们也可以通过 type 属性来指定 email 等特殊的 text 类型。HTML 支持的 验证属性有:

  • required, 必填字段,不能为空
  • pattern, 通过一个正则来验证输入
  • minlength/maxlength, 输入文本的长度
  • min/max 数字的区间
  • type, email 等等类型

在 JS 中可以调用 form.checkValidity() 函数获得检查结果。最好在 form 中添加 novalidate 属性,否则浏览器就会在校验失败的时候直接不触发 submit 事件了。

当表单验证不通过时,input 会获得一个 :invalid 的伪类。但是比较坑爹的是,当表单还没有 填写的时候,验证就已经生效了,这样对用户非常不友好,我们可以设定一个类,比如 display-errors, 只有当用户尝试提交表单之后再应用这个类就好啦~

displayErrors, setDisplayErrors = useState(false);

function handleSubmit(e) {
  if (!e.target.checkValidity()) {
    setDisplayError(true);
  }
}

return (
  <form
    onSubmit={handleSubmit}
    noValidate  // 即使校验失败也要触发 submit event,这样控制权才会到 JSclassName={displayErrors ? 'displayErrors' : ''}
  >
    {/* ... */}
  </form>
)
.displayErrors input:invalid {
  border-color: red;
}

react-hook-form

对于更复杂的逻辑,推荐使用 react-hook-form

react-hook-form v7 中使用了未受控组件,这样性能更好。结合 tailwind UI,给原生组件写样式 也更方便一点。RHF 可以使用 html 原生的验证,还可以配合使用 Yup 等第三方库来做表单验证。

  • 使用 useForm 获取操作表单的函数
  • defaultValues 用于指定默认值,也可以使用 input 的 defaultValue 属性
  • ...register("name", {options}) 用于指定input 的属性
import React from "react";
import { useForm, SubmitHandler } from "react-hook-form";

interface FormValues {
  firstName string,
  lastName string,
  email string,
}

export default function App() {
  const { register, handleSubmit } = useForm<FormValues>({
    defaultValues: {
      firstName: "bill",
      lastName: "luo",
      email: "bluebill1049@hotmail.com"
    }
  });
  const onSubmit: SubmitHandler<FormValuse> = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input {...register("email")} />
      <button type="submit">Submit</button>
    </form>
  );
}

阻止重复提交

默认情况下,当用户提交特别快,而网速不够快的时候,可能会产生重复提交的现象。使用 RHF 可以 很简单得阻止这种情况。

import {useForm} from 'react-hook-form'

export default function App() {
-  const {handleSubmit, register} - useForm({})
+  const {handleSubmit, register, formState} - useForm({})
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input {...register("email")} />
-     <button type="submit">Submit</button>
+     <button type="submit" disabled={formState.isSubmitting}>Submit</button>
    </form>
  )
}

不过,前端阻止用户重复提交只是交互上更加友好,从安全角度来说很容易绕过。因此,还需要在后端 也采取防止重复提交的措施,这样从逻辑上来说才比较完备。

antd 表单

表单的基本元素是 <Form/>, Form 中嵌套 Form.Item, 其中再嵌套 Input/TextArea 等组件。

表单的数据都在 form 元素中,使用 const form = Form.useForm() 来获取,并通过 form={form} 传递给 Form 控件。

设置了 name 属性的 Form.Item 会被 form 接管,即:

  1. 不需要通过 onChange 来更新属性值。当然依然可以通过 onChange 监听
  2. 不能使用 value 和 defaultValue,只能使用 form 的 initialValues/setFieldsValue 设置

总体来说,antd 的表单还是封装过度了,非常难以定制化,样式也不好更改。

参考

  1. https://reactjs.org/docs/uncontrolled-components.html
  2. Controlled and uncontrolled form inputs in React don't have to be complicated
  3. How to handle forms with just React
  4. https://reactjs.org/docs/forms.html
  5. https://jsfiddle.net/everdimension/5ry2wdaa/
  6. https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
  7. https://www.cnblogs.com/wonyun/p/6023363.html
  8. https://stackoverflow.com/questions/23427384/get-form-data-in-reactjs
  9. https://github.com/jquense/yup
  10. https://react-hook-form.com/faqs
  11. https://github.com/react-hook-form/react-hook-form/issues/834

© 2016-2022 Yifei Kong. Powered by ynotes

All contents are under the CC-BY-NC-SA license, if not otherwise specified.

Opinions expressed here are solely my own and do not express the views or opinions of my employer.