翻譯 | 玩轉 React 表單 —— Refs 的運用

iKcamp發表於2017-07-03

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 好。

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

相關文章