翻译 | React 表单: Refs 的运用

翻译 | React 表单: Refs 的运用

React 提供了两种从 <form> 元素中获取值的标准方法。第一种方法是实现所谓的受控组件 (可以看我博客里发表的文章) ,第二种方法是使用 React 的 ref 属性。

受控组件很重,被展示的值和组件的 state 绑定是它的特性。我们通过执行一个附着在 form 元素上的 onChange 事件句柄,来更新被展示的值。onChange 函数更新 state 属性,进而更新 form 元素的值。

(在看到下面的文章之前,如果你只是想看相应的示例代码:请移步这里

受控组件示例:

import React, { Component } from 'react';

class ControlledCompExample extends Component {
  constructor() {
    super();
    this.state = {
      fullName: ''
    }
  }
  handleFullNameChange = (e) => {
    this.setState({
      fullName: e.target.value
    })
  }
  handleSubmit = (e) => {
    e.preventDefault();
    console.log(this.state.fullName)
  }
  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <label htmlFor="fullName">Full Name</label>
          <input
            type="text"
            value={this.state.fullName}
            onChange={this.handleFullNameChange}
            name="fullName" />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

export default ControlledCompExample;

input 的值是 this.state.fullName (在第7行和第26行)。 onChange 函数是 handleFullNameChange (第 10 - 14 行和第 27 行)。

受控组件最主要的优势是:
1、便于验证用户的输入
2、可以根据受控组件的值动态地渲染其他组件。例如:一个用户在下拉列表中选择的值(如“dog” 或者 “cat” )可以控制在 form 中渲染的其他 form 组件(例如:一个设置品种的复选框)

受控组件的缺点是要写大量的代码。你需要通过 props 把 state 属性传递给 form 元素,还需要一个函数来更新这个属性的值。

对于单一表单元素来说这真的不是什么问题 —— 但是如果你需要一个庞大并且复杂的表单(不需要动态渲染或者实时验证),过度使用受控表单会让你书写成吨的代码。

从 form 元素取值的简便的方法是使用 ref 属性。我们用不同的方式来应对不同的 form 元素和组件结构,所以这篇文章剩下的内容分为以下几个部分。

  1. 文本输入框、数字输入框和选择框
  2. 子组件通过 props 传值给父组件
  3. Radio 标签集合
  4. Checkbox 标签集合

1、文本输入框、数字输入框和选择框

使用 ref 的最简单的例子是文本和数字 input 元素。我们在 input 的 ref 属性里添加一个把 input 本身作为参数的箭头函数。我喜欢把参数命名为和元素本身一样的的名字,就像下面的第三行那个样子:

<input
  type="text"
  ref={input => this.fullName = input} />

由于该参数是 input 元素本身的别名,你可以随心所欲地为它命名:

<input
  type="number"
  ref={cashMoney => this.amount = cashMoney} />

接着你可以拿到该参数,并将它赋值给当前 class 内 this 关键字上挂载的属性(译者注:这里的 class 指的是 JSX 所处的 React 组件 class)。input(例如: DOM 节点)可以通过 this.fullNamethis.amount 来读取。它的值可以通过 this.fullName.valuethis.amount.value 来读取。

选择元素也可以用相同的方法(例如:下拉列表)。

<select
  ref={select => this.petType = select}
  name="petType">
  <option value="cat">Cat</option>
  <option value="dog">Dog</option>
  <option value="ferret">Ferret</option>
</select>

选择元素的值可以通过 this.petType.value 获取。

2、子组件通过 props 传值给父组件

通过受控组件,父组件获取子组件的值十分简单 —— 父组件中已经有这个值了(译者注:在父组件中定义)!它被传递给子组件。同时 onChange 方法也被传给子组件,用户通过与 UI 互动(译者注:触发 onChange)来更新该值。

你可以在我上篇文章的受控组件示例中看到它是如何运行的。

虽然该值已经存在于受控组件的父组件中,但是当使用 ref 的时候却不是这样。使用 ref 的时候,该值存在于 DOM 节点自身当中,必须向上与父组件通信。

要将该值从子组件传给父组件,父组件需要向子组件传递一个 钩子 。然后子组件将节点挂载到 钩子 上, 以便父组件读取。

在我们更深入的探讨之前先来看一些代码。

import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();
    console.log('first name:', this.firstName.value);
    this.firstName.value = 'Got ya!';
  }
  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <CustomInput
            label={'Name'}
            firstName={input => this.firstName = input} />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

function CustomInput(props) {
  return (
    <div>
      <label>{props.label}:</label>
      <input type="text" ref={props.firstName}/>
    </div>
  );
}

export default RefsForm;

通过上面的代码,可以看到一个 form 组件 RefForm 和一个叫做 CustomInput 的 input 组件。通常,箭头函数都是在 input 自身上面,但是从这(15 - 27 行)可以看到它是通过 props 传递的。由于箭头函数存在于父组件中,所以 this.firstName 中的 this 指向父组件。

input 子组件的值被赋给父组件的 this.firstName 属性,所以父组件可以获得子组件的值。现在,父组件中的 this.firstName 指的是子组件中的 DOM 节点(例如: CustomInput 中的 input)。

父组件不仅可以访问 input 中的 DOM 节点,还可以在父组件内给节点的值赋值。在上文的第 7 行可以看到例子。一旦表单被提交, input 的值就被设置为 “Got ya!” 。

这种方式有点让人摸不着头脑,所以请仔细揣摩并敲代码实践一下,直至完全理解。

你可能会写出来更好的 radio 和 checkbox  受控组件,但是如果你真的想要用 `ref` ,那么接下来的两部分会帮到你。

3、 Radio 标签集合

不像 text 和 number 这类 input 元素,radio 元素是成组出现的。每组中的元素都有相同的 name 属性,就像这样:

<form>
  <label>
    Cat
    <input type="radio" value="cat" name="pet" />
  </label>
  <label>
    Dog
    <input type="radio" value="dog" name="pet" />
  </label>
  <label>
    Ferret
    <input type="radio" value="ferret" name="pet" />
  </label>
  <input type="submit" value="Submit" />
</form>

在 “pet” radio 标签集合中有三个选项 —— “cat”、“dog” 和 “ferret”。

由于我们关心的是整个集合的元素,所以给每个单选框设置 ref 并不是一个好主意。遗憾的是,没有 DOM 节点是包含了 radio 集合的。

可以通过下面的三步来检索出 radio 集合的值:
1、在 form 标签上设置 ref (下面的第20行)。
2、从 form 中取出这个 radio 集合。然后它应该是 pet 集合(下面的第9行)。

  • 此处返回一个节点列表和一个值。在这种情况下,这个节点列表包含三个 input 节点和被选中的值。
  • 需要注意的是这个节点列表是个类数组,它没有数组的方法。在下一部分中还有更多关于这个话题的内容。
    3、使用 . 方法来获取这个集合的值(下面的第13行)。
import React, { Component } from 'react';

class RefsForm extends Component {

  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // radio 标签集合有 value 属性
    // 查看打印出来的数据
    console.log(pet, pet.value);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <label>
            Cat
            <input type="radio" value="cat" name="pet" />
          </label>
          <label>
            Dog
            <input type="radio" value="dog" name="pet" />
          </label>
          <label>
            Ferret
            <input type="radio" value="ferret" name="pet" />
          </label>
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

export default RefsForm;

如果你正在用子组件写一个表单也是可行的。尽管组件中会有更多的逻辑,但是从 radio 集合中获取值的方法是不变的。

import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // radio 标签集合有 value 属性
    // 查看打印出来的数据
    console.log(pet, pet.value);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <RadioSet
            setName={'pet'}
            setOptions={['cat', 'dog', 'ferret']} />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

function RadioSet(props) {
  return (
    <div>
      {props.setOptions.map(option => {
        return (
          <label
            key={option}
            style={{textTransform: 'capitalize'}}>
            {option}
            <input
              type="radio"
              value={option}
              name={props.setName} />
          </label>
        )
      })}
    </div>
  );
}

export default RefsForm;

4、 Checkbox 标签集合

和 radio 标签集合不一样, Checkbox 标签集合可能有多个值。导致获取这些值会比获取 radio 标签集合的值难一些。

可以通过下面的五步来检索出 checkbox 标签集合被选中的值:

1、在 form 标签上设置 ref (下面的第27行)。
2、从 form 中取出这个checkbox 集合。然后它应该是 pet 集合(第9行)。

  • 此处返回一个节点列表和一个值
  • 需要注意的是这个节点列表是一个类数组,它没有数组的方法,然后我们就需要进行下面的这一步 ...
    3、把这个节点列表转换成一个数组,然后就可以使用数组的方法了(第 12 行的 checkboxArray )。
    4、使用 Array.filter() 获取选中的 checkbox (第 15 行的 checkedCheckboxes )。
    5、使用 Array.map() 获取选中的 checkbox 的唯一的值(第 19 行的 checkedCheckboxesValues
import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // 把节点列表转换成一个数组
    const checkboxArray = Array.prototype.slice.call(pet);

    // 仅取出被选中的 checkbox
    const checkedCheckboxes = checkboxArray.filter(input => input.checked);
    console.log('checked array:', checkedCheckboxes);

    // 使用 .map() 方法从每个被选中的 checkbox 中把值取出来
    const checkedCheckboxesValues = checkedCheckboxes.map(input => input.value);
    console.log('checked array values:', checkedCheckboxesValues);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <label>
            Cat
            <input type="checkbox" value="cat" name="pet" />
          </label>
          <label>
            Dog
            <input type="checkbox" value="dog" name="pet" />
          </label>
          <label>
            Ferret
            <input type="checkbox" value="ferret" name="pet" />
          </label>
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

export default RefsForm;

使用子组件写 checkbox 的方法和上一部分中写 radio 的方法是一样的。

import React, { Component } from 'react';

class RefsForm extends Component {
  handleSubmit = (e) => {
    e.preventDefault();

    //  从 form 中取出节点列表
    //  它是一个类数组,没有数组的方法
    const { pet } = this.form;

    // 把节点列表转换成一个数组
    const checkboxArray = Array.prototype.slice.call(pet);

    // 仅取出被选中的 checkbox
    const checkedCheckboxes = checkboxArray.filter(input => input.checked);
    console.log('checked array:', checkedCheckboxes);

    // 使用 .map() 方法从每个被选中的 checkbox 中把值取出来
    const checkedCheckboxesValues = checkedCheckboxes.map(input => input.value);
    console.log('checked array values:', checkedCheckboxesValues);
  }

  render() {
    return (
      <div>
        <form
          onSubmit={this.handleSubmit}
          ref={form => this.form = form}>
          <CheckboxSet
            setName={'pet'}
            setOptions={['cat', 'dog', 'ferret']} />
          <input type="submit" value="Submit" />
        </form>
      </div>
    );
  }
}

function CheckboxSet(props) {
  return (
    <div>
      {props.setOptions.map(option => {
        return (
          <label
            key={option}
            style={{textTransform: 'capitalize'}}>
            {option}
            <input
              type="checkbox"
              value={option}
              name={props.setName} />
          </label>
        )
      })}
    </div>
  );
}

export default RefsForm;

结论

如果你不需要:

1、实时监视 form 元素的值(例如:为了基于用户的输入渲染之后的组件)
2、实时执行自定义验证方法

那么使用 ref 方法获取 form 元素的值是一个很好的方法。

大多数情况下,越过受控组件使用 ref 最主要的价值是会写更少的代码。 checkbox ( radio 其次)是一个特例。对于 checkbox ,使用 ref 省下的代码量是很少的,所以无法说是使用受控组件好还是 ref 好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容