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

劉小夕發表於2019-10-29

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

更多文章可戳: github.com/YvetteLau/B…

Refs 使用場景

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

  • 管理焦點,文字選擇或媒體播放。
  • 觸發強制動畫。
  • 整合第三方 DOM 庫。

設定 Refs

1. createRef

支援在函式元件和類元件內部使用

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

建立 Refs

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

訪問 Refs

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

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() {
        //把 <input> ref 關聯到建構函式中建立的 `inputRef` 上
        return (
            <input type="text" ref={this.inputRef}/>
        )
    }
}
複製程式碼

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 元素時,對該節點的引用可以在 refcurrent 屬性中訪問。

import React from 'react';

export default function MyInput(props) {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        inputRef.current.focus();
    });
    return (
        <input type="text" ref={inputRef} />
    )
}
複製程式碼

關於 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 (
        <>
            <input type="text" ref={inputRef}/>
            <button onClick={() => 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 (
            <input type="text" ref={this.setTextInputRef}/>
        )
    }
}
複製程式碼

React 會在元件掛載時,呼叫 ref 回撥函式並傳入 DOM元素(或React例項),當解除安裝時呼叫它並傳入 null。在 componentDidMountcomponentDidUpdate 觸發前,React 會保證 Refs 一定是最新的。

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

import React from 'react';

export default function Form() {
    let ref = null;
    React.useEffect(() => {
        //ref 即是 MyInput 中的 input 節點
        ref.focus();
    }, [ref]);

    return (
        <>
            <MyInput inputRef={ele => ref = ele} />
            {/** other code */}

        </>
    )
}

function MyInput (props) {
    return (
        <input type="text" ref={props.inputRef}/>
    )
}
複製程式碼

4. 字串 Refs(過時API)

函式元件內部不支援使用 字串 refs [支援 createRef | useRef | 回撥 Ref]

function MyInput() {
    return (
        <>
            <input type='text' ref={'inputRef'} />
        </>
    )
}
複製程式碼

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

類元件

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

class MyInput extends React.Component {
    componentDidMount() {
        this.refs.inputRef.focus();
    }
    render() {
        return (
            <input type='text' ref={'inputRef'} />
        )
    }
}
複製程式碼

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 ref={forwardedRef} {...remainingProps}/>
            )
        }
    }
    //指定 displayName.   未複製靜態方法(重點不是為了講 HOC)
    ProxyComponent.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
    //複製非 React 靜態方法
    hoistNonReactStatic(ProxyComponent, WrappedComponent);
    return ProxyComponent;
}
複製程式碼

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

class MyInput extends React.Component {
    render() {
        return (
            <input type="text" {...this.props} />
        )
    }
}

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

React.forwardRef

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

轉發 ref 到DOM中:

import React from 'react';

const MyInput = React.forwardRef((props, ref) => {
    return (
        <input type="text" ref={ref} {...props} />
    )
});
function Form() {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        console.log(inputRef.current);//input節點
    })
    return (
        <MyInput ref={inputRef} />
    )
}
複製程式碼
  1. 呼叫 React.useRef 建立了一個 React ref 並將其賦值給 ref 變數。
  2. 指定 ref 為JSX屬性,並向下傳遞 <MyInput ref={inputRef}>
  3. React 傳遞 refforwardRef 內函式 (props, ref) => ... 作為其第二個引數。
  4. 向下轉發該 ref 引數到 <button ref={ref}>,將其指定為JSX屬性
  5. 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 ref={forwardedRef} {...remainingProps}/>
            )
        }
    }
    
    //我們在使用被withData包裝過的元件時,只需要傳 ref 即可
    const forwardRef = React.forwardRef((props, ref) => (
        <ProxyComponent {...props} forwardedRef={ref} />
    ));
    //指定 displayName.
    forwardRef.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
    return hoistNonReactStatic(forwardRef, WrappedComponent);
}

複製程式碼
class MyInput extends React.Component {
    render() {
        return (
            <input type="text" {...this.props} />
        )
    }
}
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 ref={inputRef} />
    )
}
複製程式碼

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

舊版本中(V4 / V5)

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

//MyInput.js
import React from 'react';
import { connect } from 'react-redux';

class MyInput extends React.Component {
    render() {
        return (
            <input type="text" />
        )
    }
}
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 store={store}>
            <MyInput ref={ref} />
        </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 的第四個引數 optionforwardReftrue,具體可見下面的示例:

//MyInput.js 檔案
import React from 'react';
import { connect } from 'react-redux';

class MyInput extends React.Component {
    render() {
        return (
            <input type="text" />
        )
    }
}
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 store={store}>
            <MyInput ref={ref} />
        </Provider>
    )
}


ReactDOM.render(<Main />, document.getElementById("root"));
複製程式碼

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

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

if (forwardRef) {
    const forwarded = React.forwardRef(function forwardConnectRef(
        props,
        ref
    ) {
        return <Connect {...props} forwardedRef={ref} />
    })

    forwarded.displayName = displayName
    forwarded.WrappedComponent = WrappedComponent
    return hoistStatics(forwarded, WrappedComponent)
}

//...
const { forwardedRef, ...wrapperProps } = props
const renderedWrappedComponent = useMemo(
    () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
    [forwardedRef, WrappedComponent, actualChildProps]
)
複製程式碼

參考連結:


關注公眾號,加入技術交流群

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

相關文章