Refs 提供了一種方式,允許我們訪問 DOM 節點或在 render 方法中創建的 React 元素。
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