如何在react中规范地使用ref

355 阅读3分钟

1.ref类型:

  1. React.createRef(): 适用于类组件 版本16.3+
  2. React.useRef(): 适用于函数组件 版本16.8+
  3. 回调Ref: 都能用,但非版本不支持不建议用 版本 16.2-
  4. 字符串ref: 强烈不建议使用,本文也不做相关介绍

2.不要在hooks中使用React.createRef()

import React from 'react'

function Comp() {
  const inputRef = React.createRef()
  const [num, setNum] = useState(0)

  const focus = () => inputRef.current.focus()
  const update = () => setNum(num + 1)

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focus}>聚焦</button>
      <button onClick={update}>点击触发更新 {num}</button>
    </>
  )
}

我们先来看段代码,如果你跑一下上面这段代码,会发现功能很正。这时候你可能会觉得没有影响,那我们不妨将上面的代码稍微改造一下。

import React from 'react'

function Comp() {
  const inputRef = createRef()
  const [num, setNum] = useState(0)

  useEffect(() => {
    setNum(1) // 触发一个更新
    setTimeout(() => {
      inputRef.current.focus() // 此时input能否正常聚焦?
    }, 3000)
  }, [])

  return <input ref={inputRef} />
}

如果改成这样就会发现,刚刚还正常的现在居然没办法正常聚焦了,为什么会出现这种情况呢,下面来看看具体原因:

image.png 这是一张react16.4+的生命周期图谱,从以上我们可以获取到如下两个信息:

1.Ref将会在在组件挂载时的render阶段绑定。
2.Ref将在组件卸载时的commit阶段将解除。

hooks组件的每次更新都是通过重新调用该函数来实现的,此时里面的变量都会重新被创建。我们假设触发更新前的inputRef为inputRef1,触发更新后的inputRef为inputRef2,那么在定时器中的inputRef是inputRef1还是inputRef2呢,这里涉及到闭包的概念,对闭包不熟悉的朋友可以先点击此处了解一下闭包。如果对闭包熟悉的话应该不难知道,此时的inputRef指的是inputRef1,那inputRef1对象这时候的值在上个周期中已经被卸载了,也就是此时inputRef1的值应该是{current: null},所以这个时候调用inputRef.current.focus()会直接报错。

2.React.forwardRef() 适用版本:16.3+

我们在使用React.createRef()与React.useRef()的时候还会存在以下两个问题:

1.无法在函数组件上使用ref属性。
2.在类组件上使用ref属性,只会得到组件实例。

如果父组件的 Ref 对象要传递给子组件的某个 DOM 节点或者更下层,唯一方法只有变通地使用特殊的属性名来传递 Ref 对象。自 React 16.3+ 起,可以使用 React.forwardRef() 方案。使用方法如下:

import React, { useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';

// 实现 ref 的转发
const FancyButton = React.forwardRef((props, ref) => (
  <div>
    <input ref={ref} type="text" />
    <button>{props.children}</button>
  </div>
));

// 父组件中使用子组件的 ref
function App() {
  const ref = useRef();
  const handleClick = useCallback(() => ref.current.focus(), [ ref ]);

  return (
    <div>
      <FancyButton ref={ref}>Click Me</FancyButton>
      <button onClick={handleClick}>获取焦点</button>
    </div>
  )
}

ReactDOM.render(<App />, root);

不过光看上面这个代码,似乎并不符合平时开发中的使用习惯,也会导致代码可读性变差,于是在16.8+的版本中,官方引入了React.useImperativeHandle()。下面我们将尝试用React.useImperativeHandle api对上面这段代码进行简单的改造。

3.React.useImperativeHandle() 适用版本: 16.8+

import React, { useRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';

const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));

  return <input ref={inputRef} type="text" />
});

const App = props => {
  const fancyInputRef = useRef();

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button
        onClick={() => fancyInputRef.current.focus()}
      >父组件调用子组件的 focus</button>
    </div>
  )
}

ReactDOM.render(<App />, root);

React.useImperativeHandle实际上就是提供了一个让开发者可以自定义current值的方法,虽然我们仍然需要配合forwardRef使用,但在代码的可读性上来说有了比较明显的提升,也更符合我们平时编程习惯了。

4.回调Ref

回调Ref的使用没有限制,既可以在class component中使用也可以在function component中使用,使用方式如下

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 一定是最新的。

同时回调ref也是支持在组件间传递的


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}/>
    )
}

参考链接:

  1. reactjs.org/docs/hooks-…
  2. reactjs.org/docs/react-…
  3. reactjs.org/docs/glossa…