你想知道的關於 Refs 的知識都在這了

Refs 提供了一種方式,允許我們訪問 DOM 節點或在 render 方法中創建的 React 元素。

我自己是一名從事了多年開發的web前端老程序員,目前辭職在做自己的web前端私人定製課程,今年年初我花了一個月整理了一份最適合2019年學習的web前端學習乾貨,各種框架都有整理,送給每一位前端小夥伴,想要獲取的可以關注我的頭條號並在後臺私信我:前端,即可免費獲取

Refs 使用場景

在某些情況下,我們需要在典型數據流之外強制修改子組件,被修改的子組件可能是一個 React 組件的實例,也可能是一個 DOM 元素,例如:

管理焦點,文本選擇或媒體播放。觸發強制動畫。集成第三方 DOM 庫。

設置 Refs

1. createRef

支持在函數組件和類組件內部使用

createRef 是 React16.3 版本中引入的。

創建 Refs

使用 React.createRef() 創建 Refs,並通過 ref 屬性附加至 React 元素上。通常在構造函數中,將 Refs 分配給實例屬性,以便在整個組件中引用。

訪問 Refs

當 ref 被傳遞給 render 中的元素時,對該節點的引用可以在 ref 的 current 屬性中訪問。

import React from 'react';
export default class MyInput extends React.Component {
constructor(props) {
super(props);
//分配給實例屬性
this.inputRef = React.createRef(null);
}
componentDidMount() {
//通過 this.inputRef.current 獲取對該節點的引用
this.inputRef && this.inputRef.current.focus();
}
render() {
//把 ref 關聯到構造函數中創建的 `inputRef` 上
return (

)
}
}

ref 的值根據節點的類型而有所不同:

當 ref 屬性用於 HTML 元素時,構造函數中使用 React.createRef() 創建的 ref 接收底層 DOM 元素作為其 current 屬性。當 ref 屬性用於自定義的 class 組件時, ref 對象接收組件的掛載實例作為其 current 屬性。不能在函數組件上使用 `ref` 屬性,因為函數組件沒有實例。

總結:為 DOM 添加 ref,那麼我們就可以通過 ref 獲取到對該DOM節點的引用。而給React組件添加 ref,那麼我們可以通過 ref 獲取到該組件的實例【不能在函數組件上使用 ref 屬性,因為函數組件沒有實例】。

2. useRef

僅限於在函數組件內使用

useRef 是 React16.8 中引入的,只能在函數組件中使用。

創建 Refs

使用 React.useRef() 創建 Refs,並通過 ref 屬性附加至 React 元素上。

const refContainer = useRef(initialValue);


useRef 返回的 ref 對象在組件的整個生命週期內保持不變

訪問 Refs

當 ref 被傳遞給 React 元素時,對該節點的引用可以在 ref 的 current 屬性中訪問。

import React from 'react';
export default function MyInput(props) {
const inputRef = React.useRef(null);
React.useEffect(() => {
inputRef.current.focus();
});
return (

)
}

關於 React.useRef() 返回的 ref 對象在組件的整個生命週期內保持不變,我們來和 React.createRef() 來做一個對比,代碼如下:

import React, { useRef, useEffect, createRef, useState } from 'react';
function MyInput() {
let [count, setCount] = useState(0);
const myRef = createRef(null);
const inputRef = useRef(null);
//僅執行一次
useEffect(() => {
inputRef.current.focus();
window.myRef = myRef;
window.inputRef = inputRef;
}, []);
useEffect(() => {
//除了第一次為true, 其它每次都是 false 【createRef】
console.log('myRef === window.myRef', myRef === window.myRef);
//始終為true 【useRef】
console.log('inputRef === window.inputRef', inputRef === window.inputRef);
})
return (
<>

<button> setCount(count+1)}>{count}/<button>
>
)
}

3. 回調 Refs

支持在函數組件和類組件內部使用

React 支持 回調 refs 的方式設置 Refs。這種方式可以幫助我們更精細的控制何時 Refs 被設置和解除。

使用 回調 refs 需要將回調函數傳遞給 React元素 的 ref 屬性。這個函數接受 React 組件實例 或 HTML DOM 元素作為參數,將其掛載到實例屬性上,如下所示:

import React from 'react';
export default class MyInput extends React.Component {
constructor(props) {
super(props);


this.inputRef = null;
this.setTextInputRef = (ele) => {
this.inputRef = ele;
}
}
componentDidMount() {
this.inputRef && this.inputRef.focus();
}
render() {
return (

)
}
}

React 會在組件掛載時,調用 ref 回調函數並傳入 DOM元素(或React實例),當卸載時調用它並傳入 null。在 componentDidMount 或 componentDidUpdate 觸發前,React 會保證 Refs 一定是最新的。

可以在組件間傳遞迴調形式的 refs.

import React from 'react';
export default function Form() {
let ref = null;
React.useEffect(() => {
//ref 即是 MyInput 中的 input 節點
ref.focus();
}, [ref]);
return (
<>
<myinput> ref = ele} />
{/** other code */}
>
)
}
function MyInput (props) {
return (

)
}
/<myinput>

4. 字符串 Refs(過時API)

函數組件內部不支持使用 字符串 refs [支持 createRef | useRef | 回調 Ref]

function MyInput() {
return (
<>

>
)
}

類組件

通過 this.refs.XXX 獲取 React 元素。

class MyInput extends React.Component {
componentDidMount() {
this.refs.inputRef.focus();
}
render() {
return (

)
}
}

Ref 傳遞

在 Hook 之前,高階組件(HOC) 和 render props 是 React 中複用組件邏輯的主要手段。

儘管高階組件的約定是將所有的 props 傳遞給被包裝組件,但是 refs 是不會被傳遞的,事實上, ref 並不是一個 prop,和 key 一樣,它由 React 專門處理。

這個問題可以通過 React.forwardRef (React 16.3中新增)來解決。在 React.forwardRef 之前,這個問題,我們可以通過給容器組件添加 forwardedRef (prop的名字自行確定,不過不能是 ref 或者是 key).

React.forwardRef 之前

import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';


const withData = (WrappedComponent) => {
class ProxyComponent extends React.Component {
componentDidMount() {
//code
}
//這裡有個注意點就是使用時,我們需要知道這個組件是被包裝之後的組件
//將ref值傳遞給 forwardedRef 的 prop
render() {
const {forwardedRef, ...remainingProps} = this.props;
return (
<wrappedcomponent>
)
}
}
//指定 displayName. 未複製靜態方法(重點不是為了講 HOC)
ProxyComponent.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
//複製非 React 靜態方法
hoistNonReactStatic(ProxyComponent, WrappedComponent);
return ProxyComponent;
}

這個示例中,我們將 ref 的屬性值通過 forwardedRef 的 prop,傳遞給被包裝的組件,使用:

class MyInput extends React.Component {
render() {
return (

)
}
}
MyInput = withData(MyInput);
function Form(props) {
const inputRef = React.useRef(null);
React.useEffect(() => {
console.log(inputRef.current)
})
//我們在使用 MyInput 時,需要區分其是否是包裝過的組件,以確定是指定 ref 還是 forwardedRef
return (


<myinput>
)
}
React.forwardRef

Ref 轉發是一項將 ref 自動地通過組件傳遞到其一子組件的技巧,其允許某些組件接收 ref,並將其向下傳遞給子組件。

轉發 ref 到DOM中:

import React from 'react';
const MyInput = React.forwardRef((props, ref) => {
return (

)
});
function Form() {
const inputRef = React.useRef(null);
React.useEffect(() => {
console.log(inputRef.current);//input節點
})
return (
<myinput>
)
}
調用 React.useRef 創建了一個 React ref 並將其賦值給 ref 變量。指定 ref 為JSX屬性,並向下傳遞React 傳遞 ref 給 forwardRef 內函數 (props, ref) => … 作為其第二個參數。向下轉發該 ref 參數到 ,將其指定為JSX屬性當 ref 掛載完成,inputRef.current 指向 input DOM節點

注意

第二個參數 ref 只在使用 React.forwardRef 定義組件時存在。常規函數和 class 組件不接收 ref參數,且 props 中也不存在 ref。

在 React.forwardRef 之前,我們如果想傳遞 ref 屬性給子組件,需要區分出是否是被HOC包裝之後的組件,對使用來說,造成了一定的不便。我們來使用 React.forwardRef 重構。

import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';
function withData(WrappedComponent) {
class ProxyComponent extends React.Component {
componentDidMount() {
//code
}
render() {
const {forwardedRef, ...remainingProps} = this.props;
return (
<wrappedcomponent>
)
}
}
//我們在使用被withData包裝過的組件時,只需要傳 ref 即可
const forwardRef = React.forwardRef((props, ref) => (
<proxycomponent>
));
//指定 displayName.
forwardRef.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
return hoistNonReactStatic(forwardRef, WrappedComponent);
}
class MyInput extends React.Component {
render() {
return (

)
}
}
MyInput.getName = function() {
console.log('name');
}
MyInput = withData(MyInput);
console.log(MyInput.getName); //測試靜態方法拷貝是否正常
function Form(props) {
const inputRef = React.useRef(null);
React.useEffect(() => {
console.log(inputRef.current);//被包裝組件MyInput
})
//在使用時,傳遞 ref 即可
return (
<myinput>
)
}

react-redux 中獲取子組件(被包裝的木偶組件)的實例

舊版本中(V4 / V5)

我們知道,connect 有四個參數,如果我們想要在父組件中子組件(木偶組件)的實例,那麼需要設置第四個參數 options 的 withRef 為 true。隨後可以在父組件中通過容器組件實例的 getWrappedInstance() 方法獲取到木偶組件(被包裝的組件)的實例,如下所示:

//MyInput.js
import React from 'react';
import { connect } from 'react-redux';
class MyInput extends React.Component {
render() {
return (

)
}
}
export default connect(null, null, null, { withRef: true })(MyInput);
//index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import MyInput from './MyInput';
function reducer(state, action) {
return state;
}
const store = createStore(reducer);
function Main() {
let ref = React.createRef();
React.useEffect(() => {
console.log(ref.current.getWrappedInstance());
})
return (
<provider>
<myinput>
/<provider>
)
}
ReactDOM.render(<main>, document.getElementById("root"));


這裡需要注意的是:MyInput 必須是類組件,而函數組件沒有實例,自然也無法通過 ref 獲取其實例。react-redux 源碼中,通過給被包裝組件增加 ref 屬性,getWrappedInstance 返回的是該實例 this.refs.wrappedInstance。

if (withRef) {
this.renderedElement = createElement(WrappedComponent, {
...this.mergedProps,
ref: 'wrappedInstance'
})
}
新版本(V6 / V7)

react-redux新版本中使用了 React.forwardRef方法進行了 ref 轉發。自 V6 版本起,option 中的 withRef 已廢棄,如果想要獲取被包裝組件的實例,那麼需要指定 connect 的第四個參數 option 的 forwardRef 為 true,具體可見下面的示例:

//MyInput.js 文件
import React from 'react';
import { connect } from 'react-redux';
class MyInput extends React.Component {
render() {
return (

)
}
}
export default connect(null, null, null, { forwardRef: true })(MyInput);

直接給被包裝過的組件增加 ref,即可以獲取到被包裝組件的實例,如下所示:

//index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import MyInput from './MyInput';
function reducer(state, action) {
return state;


}
const store = createStore(reducer);
function Main() {
let ref = React.createRef();
React.useEffect(() => {
console.log(ref.current);
})
return (
<provider>
<myinput>
/<provider>
)
}
ReactDOM.render(<main>, document.getElementById("root"));

同樣,MyInput 必須是類組件,因為函數組件沒有實例,自然也無法通過 ref 獲取其實例。

react-redux 中將 ref 轉發至 Connect 組件中。通過 forwardedRef 傳遞給被包裝組件 WrappedComponent 的 ref。

if (forwardRef) {
const forwarded = React.forwardRef(function forwardConnectRef(
props,
ref
) {
return <connect>
})
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
//...
const { forwardedRef, ...wrapperProps } = props
const renderedWrappedComponent = useMemo(
() => <wrappedcomponent>,
[forwardedRef, WrappedComponent, actualChildProps]
)

ref 和 ReactDOM.findDOMNode(ref)

可以通過 ReactDOM.findDOMNode(ref) 來獲取組件掛載後真正的 DOM 節點。

對於 HTML 元素,ref 引用的就是該元素的 DOM 節點,無需通過 ReactDOM.findDOMNode(ref) 來獲取。

ref 和 ReactDOM.findDOMNode(ref) 的區別ref 添加在組件上,獲取的是組件實例,添加到原生 HTML 上獲取的是 DOM。ReactDOM.findDOMNode(ref) 當 ref 在 HTML 上,返回的是該 DOM;當 ref 在組件上時,返回的是該組件 render 方法中的 DOM。

原文鏈接:https://mp.weixin.qq.com/s/Vwgj19mQzkFU2UNnfihsKQ