教你用 React createRef

4,654 阅读3分钟

什么是 Ref?

允许用户直接访问 DOM 节点或 React 元素。

我们知道,在 React 中写的 jsx 都是虚拟 DOM,react-dom 库的 render 方法来负责将我们的 jsx 转换为真实 DOM,也就是说,开发者在写组件的时候,还没有生成真实 DOM 呢,那如果想拿到真实 DOM 怎么办?这就需要给组件添加 ref 了,可分为以下三种场景:

  • 给原生组件添加 ref
  • 给类组件添加 ref
  • 给函数组件添加 ref

接下来分别进行介绍:

给原生组件添加 ref

如果给一个原生组件添加了 ref 属性,会把真实的 DOM 元素赋值给 ref.current,下面是一个具体的案例,给 input 输入框添加 ref,在 button 点击事件触发时,让 input 框获取焦点:

import React from 'react'

class Form extends React.Component {
  constructor(props) {
    super(props)
    this.ref = React.createRef()
  }
  focus = () => {
    this.ref.current.focus()
  }
  render() {
    return (
      <div>
        <input ref={this.ref} />
        <button onClick={this.focus}>获取焦点</button>
      </div>
    )
  }
}

export default Form

代码非常浅显易懂,this.ref.current 其实就是指代了原生 input DOM 节点,实现的效果就是点击 button 之后触发 input 获取焦点:

给类组件添加 ref

如果我们自己写了一个组件,给它绑定 ref 会是什么效果呢?例如创建 Text 自定义组件,里面渲染了原生 input 元素:

import React from 'react'

class Text extends React.Component {
  constructor(props) {
    super(props)
    this.ref = React.createRef()
  }
  focus = () => {
    this.ref.current.focus()
  }
  render() {
    return <input ref={this.ref} />
  }
}

export default Text

我们在 Form 组件中,引入这个 Text 组件,给它绑定 ref:

import React from 'react'
import Text from './Text'

class Form extends React.Component {
  constructor(props) {
    super(props)
    this.ref = React.createRef()
  }
  focus = () => {
    this.ref.current.focus()
  }
  render() {
    return (
      <div>
        <Text ref={this.ref} />
        <button onClick={this.focus}>获取焦点</button>
      </div>
    )
  }
}

export default Form

运行发现,效果和之前是一模一样的。其实给自定义组件绑定 ref 就相当于拿到了该组件的实例,有了实例就能调用它的任意方法了,所以上面的逻辑就是:

  • Form 组件通过 ref 拿到 Text 组件的实例
  • 点击按钮后,触发 Text 组件实例的 focus 方法
  • Text 组件 focus 方法里面触发了 input 原生组件的 focus 方法

给函数组件添加 ref

函数组件跟类组件不同,它没有实例,所以当我们给自定义函数组件添加 ref 的时候,会报错:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

为了能够引用函数组件,我们必须通过 React.forwardRef 进行转发,代码如下:

import React from 'react'

function Text(props, forwardRef) {
  return <input ref={forwardRef} />
}

export default React.forwardRef(Text)

当在 Form 组件给 Text 函数组件添加 ref 的时候,其实 ref 会被转发到 Text 函数组件的第二个参数里面,在函数组件内部,可以把这个 forwardRef 绑定到任意的组件上面,达到转发的效果。如果 Form 组件也改成函数组件的话,可以这么写:

import React from 'react'
import Text from './Text'

function Form() {
  const ref = React.useRef()
  return (
    <div>
      <Text ref={ref} />
      <button onClick={() => ref.current.focus()}>获取焦点</button>
    </div>
  )
}

export default Form

由于 forwardRef 转发之后,把 Text 组件内部的元素直接暴露给外部调用方了,这样并不安全(类比类组件 ref 只能访问实例的方法),可以通过 useImperativeHandle 自定义向外暴露的对象,使用方法如下:

import React from 'react'

function Text(props, forwardRef) {
  const ref = React.useRef()
  React.useImperativeHandle(forwardRef, () => ({
    focus: () => ref.current.focus()
  }))
  return <input ref={ref} />
}

export default React.forwardRef(Text)

useImperativeHandle 函数的第一个参数就是被转发的 ref,第二个参数是一个函数,返回值是用户自定义的想对外暴露的对象。