[TOC]
`React`中的`Refs`提供了一種訪問`render()`方法中創建的`React`元素(或`DOM`節點)的方法。
當父組件需要與子組件交互時,我們通常使用 [props](https://reactjs.org/docs/components-and-props.html) 來傳遞相關信息。 但是,**在某些情況下,我們可能需要修改子項,而不用新的`props`重新呈現 (re-rendering) 它**。這時候就需要`refs`出場了。
# 什么時候使用 Refs ?
我們建議在以下情況下使用 `refs`:
* 與第三方 `DOM` 庫集成
* 觸發命令式動畫
* 管理焦點,文本選擇或媒體播放
> 譯注:第三點是否也可以理解為使用 `event` 對象呢?在 React 中就是合成事件 (SyntheticEvent)。
> **官方文檔中提到:避免使用 `refs` 來做任何可以通過聲明式實現來完成的事情**。
所以一旦我們確定我們需要使用 `refs`,我們如何使用它們呢?
# 在 React 中使用 Refs
您可以通過多種方式使用`refs`:
* [React.createRef()](https://reactjs.org/docs/refs-and-the-dom.html)
* 回調引用 (Callback refs)
* String refs(已過時,這 API 將被棄用)
* 轉發`refs`(Forwarding refs)
## `React.createRef()`
使用`React.createRef()`創建引用,并通過`ref`屬性附加到React元素上。
在構造組件時,通常將 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>
);
}
}
```
在上面的代碼塊中,我們構建了一個按鈕,當單擊它時,**該頁面會自動聚焦在輸入框上。**
首先,我們在構造方法中**創建一個 React `ref` 實例**,并將其賦值給 `this.textInput`,然后通過`ref` 屬性將其分配給 `input`元素。
```
<input?type="text"?ref={this.textInput} />
```
注意,當 `ref` 屬性被一個`HTML` 元素使用時(比如當前示例中的 `input`元素),在 `constructor` 中使用 `React.createRef()` 創建的 `ref`會接收 **來自底層`DOM`元素的 `current`值**。
> 譯注:這里的 `current` 應該是[合成事件(SyntheticEvent)](http://react.html.cn/docs/events.html)
這意味著訪問 `DOM` 值,我們需要寫這樣的東西:
```
this.textInput.current;
```
## Refs 回調
**Refs 回調** 是在 React 中使用 `ref` 的另一種方式。要以這種方式使用 ref,我們需要為 ref 屬性設置回調函數。
當我們設置 ref 時,React 會調用這個函數,并將 element 作為第一個參數傳遞給它。
```
// 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` 回調會在 `componentDidMount` 和 `componentDidUpdate` 生命周期之前調用。)
## `React.forwardRef`
不能在函數組件上使用`ref`屬性,因為函數組件沒有實例。
如果您希望引用函數組件,您可以使用`forwardRef`可能與`useImperativeHandle`結合使用),或者您可以將它轉換為類組件。
```
// Ref.js
// 普通的函數組件 是不會有 ref 參數的,React.forwardRef 返回一個組件
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();
```
然后,我們**通過組件 `<TextInput ref={inputRef}>` 的 `ref` 屬性的值,將 `ref` 向下傳遞**。然后`React` 將會把 `ref` 作為第二個參數轉發給 `forwardRef` 函數(這個是在 React 框架層面完成的事情)。
接下來,我們將此 `ref` 參數轉發給`<input ref={ref}>`。現在可以在外層組件通過 `inputRef.current` 訪問 DOM 節點的值了。
## 高階組件的 ref
如果我要操作一個高階組件 的 DOM ,怎么辦?
ref 是不能像 props 一樣,往下面傳遞的,因此想要往下面傳遞,必須要用到`React.forwardRef`這個 API。
```
// by 司徒正美
const ThemeContext = React.createContext('light');
class ThemeProvider extends React.Component {
state = {theme: 'light'};
render() {
return (
<ThemeContext.Provider value={this.state.theme}>
{this.props.children}
</ThemeContext.Provider>
);
}
}
class FancyButton extends React.Component {
buttonRef = React.createRef();
focus() {
this.buttonRef.current.focus();
}
render() {
const {label, theme, ...rest} = this.props;
return (
<button
{...rest}
className={`${theme}-button`}
ref={this.buttonRef}>
{label}
</button>
);
}
}
function withTheme(Component) {
// React.forwardRef 會提供 第二個參數"ref",然后就可以直接把其附加到組件上
function ThemedComponent(props, ref) {
return (
<ThemeContext.Consumer>
{theme => (
<Component {...props} ref={ref} theme={theme} />
)}
</ThemeContext.Consumer>
);
}
// These next lines are not necessary,
// But they do give the component a better display name in DevTools,
// e.g. "ForwardRef(withTheme(MyComponent))"
const name = Component.displayName || Component.name;
ThemedComponent.displayName = `withTheme(${name})`;
// 告訴 React 傳遞 "ref" 到 ThemedComponent.
return React.forwardRef(ThemedComponent);
}
const fancyButtonRef = React.createRef();
const FancyThemedButton = withTheme(FancyButton);
// fancyButtonRef 現在指向 FancyButton
<FancyThemedButton
label="Click me!"
onClick={handleClick}
ref={fancyButtonRef}
/>;
```
# 結論
與通過`props`和`state`不同,`Refs`是一種將數據傳遞給特定子實例的好方法。
你必須要小心,因為`refs`操縱實際的`DOM`,而不是虛擬的`DOM`,這與`React`思維方式相矛盾。因此,雖然`refs`不應該是通過應用程序流動數據的默認方法,但是當您需要時,它們是可以從`DOM`元素讀取數據的好方法。
# 參考
推薦下「司徒正美」大佬的[React v16.3.0: New lifecycles and context API](https://segmentfault.com/a/1190000014083970),createRef API,forwardRef API 中的示例可以作為補充閱讀。
[https://segmentfault.com/a/1190000019277029](https://segmentfault.com/a/1190000019277029)