你想知道的关于 Refs 的知识都在这了

Refs 提供了一种方式,容许咱们访问 DOM 节点或在 render 方法中建立的 React 元素。html

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

Refs 使用场景

在某些状况下,咱们须要在典型数据流以外强制修改子组件,被修改的子组件多是一个 React 组件的实例,也多是一个 DOM 元素,例如:git

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。

设置 Refs

1. createRef

支持在函数组件和类组件内部使用github

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

建立 Refs

使用 React.createRef() 建立 Refs,并经过 ref 属性附加至 React 元素上。一般在构造函数中,将 Refs 分配给实例属性,以便在整个组件中引用。数组

访问 Refs

ref 被传递给 render 中的元素时,对该节点的引用能够在 refcurrent 属性中访问。app

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 的值根据节点的类型而有所不一样:dom

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 建立的 ref 接收底层 DOM 元素做为其 current 属性。
  • ref 属性用于自定义的 class 组件时, ref 对象接收组件的挂载实例做为其 current 属性。
  • 不能在函数组件上使用 ref 属性,由于函数组件没有实例。

总结:为 DOM 添加 ref,那么咱们就能够经过 ref 获取到对该DOM节点的引用。而给React组件添加 ref,那么咱们能够经过 ref 获取到该组件的实例【不能在函数组件上使用 ref 属性,由于函数组件没有实例】。ide

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'} />
        </>
    )
}
复制代码

类组件

经过 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]
)
复制代码

参考连接:


关注公众号,加入技术交流群