【譯】在 React 元件中使用 Refs 指南

joking_zhang發表於2019-05-23
原文:Fullstack React's Guide to using Refs in React Components
作者:Yomi Eluwande
譯者:博軒

refs

使用 React 時,我們的預設思維方式應該是 不會強制修改 DOM ,而是通過傳入 props 重新渲染元件。但是,有些情況卻無法避免修改 DOM

React 中的 Refs 提供了一種訪問 render() 方法中建立的 React 元素(或 DOM 節點)的方法。

當父元件需要與子元件互動時,我們通常使用 props 來傳遞相關資訊。 但是,在某些情況下,我們可能需要修改子項,而不用新的props 重新呈現 (re-rendering) 它。 這時候就需要 refs 出場了。

我什麼時候應該使用 Refs ?

我們建議在以下情況下使用 refs

  • 與第三方 DOM 庫整合
  • 觸發命令式動畫
  • 管理焦點,文字選擇或媒體播放
譯註:第三點是否也可以理解為使用 event 物件呢?在 React 中就是合成事件(SyntheticEvent)。
官方文件中提到:避免使用 refs 來做任何可以通過宣告式實現來完成的事情。

所以一旦我們確定我們真的應該使用 refs,我們需要如何使用它們呢?

在 React 中使用 Refs

您可以通過多種方式使用 refs :

  • React.createRef()
  • 回撥引用 (Callback refs)
  • String refs(已過時)
  • 轉發 refs (Forwarding refs)

接下來,讓我們看看每一種實現方式:

React.createRef()

可以使用該 React.createRef() 函式建立 Refs ,並通過該 ref 屬性附加到 React 元件中的 HTML 元素。

通常在元件的建構函式內建立 ref ,使其在整個元件中可用。例如:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.firstRef = React.createRef();
  }
  render() {
    return <div ref={this.firstRef} />;
  }
}複製程式碼

如上所示:

  • 一個 ref 例項在建構函式中建立,並賦值給 this.firstRef
  • render() 方法內部,將建構函式中建立的 ref 傳遞給 div

接下來,讓我們看一個在 React 元件中使用 refs 的示例。

使用 Refs 聚焦輸入

這是另一個例子:

// Ref.js
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // create a ref to store the textInput DOM element
    this.textInput = React.createRef();
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // Explicitly focus the text input using the raw DOM API
    // Note: we're accessing "current" to get the DOM node
    this.textInput.current.focus();
  }

  render() {
    // tell React that we want to associate the <input> ref
    // with the `textInput` that we created in the constructor
    return (
      <div>
        <input type="text" ref={this.textInput} />

        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}複製程式碼

示例連結

在上面的程式碼塊中,我們構建了一個按鈕,當單擊它時,該頁面會自動聚焦在輸入框上。

首先,我們在構造方法中建立一個 ref 例項,並將其賦值給 this.textInput,然後通過 ref 屬性將其分配給 input 元素。

<input type="text" ref={this.textInput} />複製程式碼

注意,當 ref 屬性被一個 HTML 元素使用時(比如當前示例中的 input 元素),在 constructor 中使用React.createRef() 建立的 ref 會接收來自底層 DOM 元素的 current 值。

譯註:這裡的 current 應該是 合成事件(SyntheticEvent)

這意味著訪問 DOM 值,我們需要寫這樣的東西:

this.textInput.current;複製程式碼

第二個元素是一個按鈕,點選它之後會自動聚焦到第一個輸入框上面。我們為 onClick 屬性設定了 this.focusTextInput 函式。

<input
  type="button"
  value="Focus the text input"
  onClick={this.focusTextInput}
/>複製程式碼

函式 focusTextInput() 使用了 JavaScript 構建 DOM 的標準函式。 .focus() 方法會將游標聚焦於文字輸入框上。

focusTextInput() {
  this.textInput.current.focus();
}複製程式碼

最後,focusTextInput 函式繫結在這樣的 constructor 方法中的:

this.focusTextInput = this.focusTextInput.bind(this);複製程式碼

從 ref 中獲取值

在這個例子中,我們將看到如何為 input 輸入框設定 ref 屬性,並通過 ref 來獲取值。示例如下:

示例連結

在這個例子中,我們建立了一個 input 輸入框來輸入值。然後,當單擊提交按鈕時,我們將讀取此值,並在控制檯列印。

// Ref.js
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // create a ref to store the textInput DOM element
    this.textInput = React.createRef();
  }
  handleSubmit = e => {
    e.preventDefault();

    console.log(this.textInput.current.value);
  };

  render() {
    // tell React that we want to associate the <input> ref
    // with the `textInput` that we created in the constructor
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <input type="text" ref={this.textInput} />
          <button>Submit</button>
        </form>
      </div>
    );
  }
}複製程式碼

同樣,我們使用該 React.createRef() 函式建立一個 ref 例項,然後將它分配給例項變數 this.textInput

render 函式中,我們希望讀取 form 下輸入框的值。我們如何讀取這個值? 通過為 input 指定一個 ref ,然後讀取 ref 的值。

<input type="text" ref={this.textInput} />複製程式碼

點選提交按鈕,上面示例中 form 元素會通過 onSubmit 方法,呼叫 this.handleSubmit 函式 ,並在控制檯列印輸入框中的資訊。

handleSubmit = e => {
  e.preventDefault();
  console.log(this.textInput);
};複製程式碼
上面,引數 e 包含事件物件。我們使用e.preventDefault() 來告訴瀏覽器我們正在處理被點選的提交按鈕,我們不希望這個事件“冒泡”(意思就是說,阻止瀏覽器的預設行為)。

譯註:這裡可以看一下 React 對於事件的處理:在 React 中另一個不同點是你不能通過返回 false 的方式阻止預設行為。你必須顯式的使用 preventDefault

在上面示例中,我們列印了 this.textInput ,在控制檯可以看到一個 ref 物件。

> Object {current: HTMLInputElement}複製程式碼

請注意,它有一個 current屬性,即 HTMLInputElement 。這是 input DOM 元素本身,而不是實際值。 我們必須使用 this.textInput.current.value 來獲取 input 標籤的實際值:

handleSubmit = e => {
  e.preventDefault();
  console.log(this.textInput.current.value);
};複製程式碼

使用 refs 是一種從表單中直接提取值的方式:只需要給 input 標籤設定 ref ,並在你需要的時候將值提取出來。

Refs 回撥

Refs 回撥 是在 React 中使用 ref 的另一種方式。要以這種方式使用 ref,我們需要為 ref 屬性設定回撥函式。當我們設定 ref 時,React 會呼叫這個函式,並將 element 作為第一個引數傳遞給它。

示例連結

這是另一個例子的程式碼。像上面的示例一樣,此程式碼獲取 input 標籤的文字值,但在這裡我們使用回撥引用:

// Refs.js
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };
  }

  handleSubmit = e => {
    e.preventDefault();
    console.log(this.textInput.value);
  };

  render() {
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <input type="text" ref={this.setTextInputRef} />
          <button>Submit</button>
        </form>
      </div>
    );
  }
}複製程式碼

上面的示例中,我們將 input 標籤的 ref 設定為 this.setTextInputRef

當元件安裝時,React 會將 DOM 元素傳遞給 ref 的回撥;當元件解除安裝時,則會傳遞 null。(ref 回撥會在 componentDidMountcomponentDidUpdate 生命週期之前呼叫。)

String Ref(已過時)

還有另一種設定 refs 的方法,但它被認為是過時的,可能很快就會被棄用。但是你可能會在其他人的程式碼中看到它,所以這裡說一下。

使用 string refs,你將會看到這樣的 input 標籤:

<input type="text" ref="textInput" />複製程式碼

然後,我們可以在組建上得到這樣的值:this.refs.textInput.value - 但是,再次宣告,這不應該在新程式碼中使用,因為這個 API 將被棄用。

轉發 Refs (Forwarding Refs)

Ref forwarding 是一種將 ref 通過元件傳遞給其子節點的技術。它對於可複用元件庫和高階元件(HOC)等情況非常有用。

示例地址

您可以使用 React.forwardRef 函式將 ref 轉發到元件。我們來看下面的例子:

// Ref.js
const TextInput = React.forwardRef((props, ref) => (
  <input type="text" placeholder="Hello World" ref={ref} />
));

const inputRef = React.createRef();

class CustomTextInput extends React.Component {
  handleSubmit = e => {
    e.preventDefault();
    console.log(inputRef.current.value);
  };

  render() {
    return (
      <div>
        <form onSubmit={e => this.handleSubmit(e)}>
          <TextInput ref={inputRef} />
          <button>Submit</button>
        </form>
      </div>
    );
  }
}複製程式碼

Ref forwarding 允許元件接收一個 ref ,並將它向下傳遞(換句話說,“轉發”它)給子元件。

在上面的示例中,我們使用 input 標籤建立了一個名為 TextInput 的元件。那麼,我們如何將 ref 傳遞或轉發到 input 標籤呢?

首先,我們使用下面的程式碼建立一個 ref

const inputRef = React.createRef();複製程式碼

然後,我們將 ref 通過為元件 <TextInput ref={inputRef}> 指定一個同名的 JSX 的屬性,將 ref 向下傳遞。然後 React 將會把 ref 作為第二個引數轉發給 forwardRef 函式。

接下來,我們將此 ref 引數轉發給 <input ref={ref}>。現在可以在外層元件通過 inputRef.current 訪問DOM節點的值了。

轉發 refs 和高階元件

最後,讓我們看一下使用 refs 的另一個例子,但這次是使用高階元件(HOC)。

示例連結

在上面的示例應用程式中,會將所有 input 標籤中輸入的值在控制檯列印。這裡已經為 input 標籤設定了 ref 屬性,接下來,讓我們看一下需要如何在高階元件中傳遞 / 轉發 ref

const Input = InputComponent => {
  const forwardRef = (props, ref) => {
    const onType = () => console.log(ref.current.value);
    return <InputComponent forwardedRef={ref} onChange={onType} {...props} />;
  };
  return React.forwardRef(forwardRef);
};複製程式碼

這裡有一個名為 Input 的高階元件 ,它接受 InputComponent 作為引數。當使用者輸入的時候,他還會將 ref 的值在控制檯列印。

Input 高階元件內,forwardRef 函式會返回 InputComponentforwardRef 函式中所包含的 ref 引數,是由 React.forwardRef 函式建立的。 高階元件最終會將包裝好的元件作為值返回。

接下來,我們建立一個元件,將 input 作為子元件包含進來。

const TextInput = ({ forwardedRef, children, ...rest }) => (
  <div>
    <input ref={forwardedRef} {...rest} />
    {children}
  </div>
);複製程式碼

上面的元件會將 forwardedRef 分配給 ref 屬性, 當渲染子元件的時候,input 輸入框就會接收到這個 ref...restprops 的解構(也就是說,我們會將 rest 陣列中的所有引數作為 props 傳遞給 input 元件)。那麼我們該如何使用 TextInput 元件呢?像這樣:

const InputField = Input(TextInput);

class CustomTextInput extends Component {
  render() {
    const inputRef = React.createRef();
    
    return <InputField ref={inputRef} />;
  }
}複製程式碼

最後,將 TextInput 傳入 Input 高階元件,會返回一個 InputField component

建立一個 ref ,並作為引數傳遞給 InputField 元件。

結論

與通過 propsstate 不同,Refs 是一種將資料傳遞給特定子例項的好方法。

你必須要小心,因為 refs 操縱實際的 DOM,而不是虛擬的 DOM,這與 React 思維方式相矛盾。因此,雖然 refs 不應該是通過應用程式流動資料的預設方法,但是當您需要時,它們是可以從 DOM 元素讀取資料的好方法。

譯註:推薦下「司徒正美」大佬的 React v16.3.0: New lifecycles and context API,createRef API,forwardRef API 中的示例可以作為補充閱讀。
本文已經聯絡原文作者,並授權翻譯,轉載請保留原文連結


相關文章