React: Concept: Form Elements

20th August 2020 at 2:19pm
React: Concept

HTML 的表单元素,如 <input type="text">, <select>, <input type="file"> 会在内部保存其状态。React 发明了两个概念:

Controlled components

这类组件会接管 HTML 表单元素的内部状态。你不再需要通过 DOM 的接口(如 HTMLInputElement.value)来获取 input 元素的值,而是通过 React state。具体做法如下:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

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

缺点是对于每个 form element,你都需要定义 onChangevalue 属性。而且有大量 validation 代码要编写。Formik 是 React 官方推荐的用来解决此问题的库。

另外有个常见的 pattern,是 即时修改用户的输入。这在 React 上很容易实现:

handleChange(event) {
  this.setState({value: event.target.value.toUpperCase()});
}

另外:

  • <textarea> 使用与 <input type="text"> 无异
  • <input type="checkbox"><input type="radio"> 使用 checked 属性(而不是 value
  • <select> 也使用 value 来表示选中了哪个对象
    • 传给 value 一个数组表示选中多个元素:
      <select multiple={true} value={['B', 'C']}>
  • 如果你不想再控制用户输入,将其 value 设为 null:
    <input value={null} />

总结起来是(来源):

ElementValue propertyChange callbackNew value in the callback
<input type="text" />value="string"onChangeevent.target.value
<input type="checkbox" />checked={boolean}onChangeevent.target.checked
<input type="radio" />checked={boolean}onChangeevent.target.checked
<textarea />value="string"onChangeevent.target.value
<select />value="option value"onChangeevent.target.value

Uncontrolled components

非受控的组件,即是说组件的状态保存在相应的 DOM 元素中。React 提供了 refs 来访问 DOM 元素:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

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

大部分情况下你都不应该用 uncontrolled 方式。但是处理文件的场景是例外,因为 <input type="file"> 的值只能由用户来设置,无法通过 React 可编程地设置:

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

ReactDOM.render(
  <FileInput />,
  document.getElementById('root')
);

Controlled v.s. uncontrolled

Gosha Arinich 有一篇非常好的 文章,对比了这两种方式。总结起来是:

featureuncontrolledcontrolled
one-time value retrieval (e.g. on submit)
validating on submit
instant field validation
conditionally disabling submit button
enforcing input format
several inputs for one piece of data
dynamic inputs

我觉得 uncontrolled 方式最大的缺点,是失去了利用 React 方便地做即时数据验证、修改数据格式(比如改成全大写)的能力。如果你用 uncontrolled,你还需要引入其他的 JS 代码来实现同样的功能。