踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑

踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第1张图片
react.jpg

什么是ref

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

上面是官网对ref的介绍,简单概括一下ref的作用为用来获取组件的实例或Dom,并且无论是你使用Vue框架还是React框架,都不建议过度使用ref,能用组件通信来解决的问题,一般不推荐使用ref,一般是作为“逃生舱”来使用,但有一些情况,你不得不使用ref获取组件的实例或者DOM,来打破典型的数据流形式组件通信。比如,我们做了某些比较死的表单封装,想直接通过父组件调用其提交方法,比如,你想让你封装的“轮播图”组件直接执行其下一步的操作,等等等,程序员可能遇到很多种奇怪的需求,也可能需要你用到ref,这里发表一下个人观点,相对于Vue这种渐进式框架而言,React从一开始就对开发者提出的比较严格规范的要求,所以React的ref并不像Vue中那样出现的平凡,多数情况下,还是依照经典的数据流来完成一些操作,虽然两个两个框架都不建议过度使用ref,比较低的出场率也就注定在使用到它的时候难免遇到一些坑,今天,我将通过这篇文章,来尽可能详细的介绍ref的相关内容,并记录我使用React + ts 访问ref时遇到的一些坑(本人,react老玩家,ts实属新手)。

环境准备

  • create-react-app
  • typescript

ref的访问方式

  • React.createRef()
  • useRef(只在函数组件中使用的hooks)
  • 回调函数
  • 字符串(已废弃,不要再使用了!)

ref 的值根据节点的类型而有所不同:

  • ref属性用于 HTML 元素时,ref为其底层 DOM 元素。
  • ref属性用于自定义 class 组件时,ref 对象为其接收组件的挂载实例。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。如果你想要在函数组件中使用ref,可以使用forwardRef,但你可以在函数组件内部使用ref属性,只要他是指向DOM元素或者class组件。

上面介绍了几个关键点,下面,我们将就上面提到的点,做详细的介绍和实例demo。

React.createRef()

app.tsx

class App extends React.PureComponent {
  childRef: any
  constructor (props:any) {
    super(props)
    this.childRef = React.createRef()
    console.log(this.childRef.current)
  }
  render () {
    return (
      
这是一个类组件
); } componentDidMount () { console.log(this.childRef.current) } } export default App;

child.tsx

import React from 'react'
class Child extends React.PureComponent {
    render () {
        return 
这是子组件
} } export default Child

上面,我们使用React.createRef(),在类组件App中访问了类组件Child的ref。我们通过React.createRef()创建refs,并通过ref属性,传给对应的子组件,因为子组件是一个类组件,那么就会将其实例,挂载到ref对象的current属性上,于是我们的打印结果是这样的。

踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第2张图片
refchild.png

结果比较明显了,这里有一个细节是,我写在 constructor中的打印结果为null,而写在 componentDidMount生命周期里才能正常打印,所以,这里有一点需要主要的是 ref是组件或者DOM挂载后才可以访问到的,这一点需要注意, 你在访问组件ref时,必须确保其已经挂载
我们上面的代码中,APP也是一个类组件,那么如果APP是函数组件呢?我们也是可以正常使用 React.createRef(),只不过纯函数组件没有生命周期,我们可以通过事件来访问(这时候组件一定是挂载完成的),通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

const App: React.FC = () => {
  const childRef: any = React.createRef()
  const getRef = () => {
    console.log(childRef.current)
  }
  return (
    
这是一个函数组件
); } export default App

我们也是可以正常获取结果的。其实对于纯函数组件,我更推荐使用useRef这个hooks来完成,因为useRef的优势还是比传统的获取ref的形式要多很多,因为useRef不仅仅可以存ref,还可以存任何值,你可以用它来存变量等,当然,这是这个hooks本身的优势,今天我们主要说ref,还是说说怎么使用useRef来访问ref吧

useRef

react推出hooks可谓是让react变得更受欢迎了,也更加的舒服了,其中的refRef就可以帮助我们在纯函数组件中访问refs对象,于是,对于上面,我们使用react.createRef()在纯函数中访问refs的代码,可以做下面的更改

import React, { useRef } from 'react';
const App: React.FC = () => {
  const childRef: any = useRef(null)
  const getRef = () => {
    console.log(childRef.current)
  }
  return (
    
这是一个函数组件
); } export default App

其结果也是一样的,useRefReact.createRef()类似,都会将ref放在其.current属性中,不过,useRef的作用要远远大于后者,这里推荐,再次推荐。具体可看官网对其的介绍。

回调函数

React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。不同于react.createRefuseRef返回一个对象,如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调ref来实现。
我们还是使用上面的代码(实际开发中,我已经很少写类组件了~),换成回调函数的形式。

import React, { useState } from 'react';
import Child from './components/child'

const App: React.FC = () => {
  const [isMount, setIsMount] = useState(true)
  let childRef: any
  const getRef = () => {
    console.log(childRef)
  }
  const setRef = (node: any) => {
    console.log('我挂载/卸载了')
    childRef = node
  }
  const unMountChild = () => {
    setIsMount(false)
  }
  return (
    
这是一个函数组件 {isMount && }
); } export default App

在上面的代码中,我们通过传入回调的方式,将refs对象赋给了childRef变量,并可以在某一事件中获取它,这一点,和其他两种方式没有差别,差别在于,我们可以在Child组件挂载或者卸载的时候执行一些方法,我们通过一个状态控制了子组件的挂载状态,来做这个demo,最终的实际效果如下


踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第3张图片
refChild2.png

我们在刚开始挂载时执行了方法,这时候我们通过事件获取其refs对象,当修改状态使组件卸载,可以看到再次执行了方法,并且这时候也获取不到refs对象了。这就是refs回调的作用。

小结:上面我们介绍了几种访问refs对象和创建refs对象的方法,这样的文章在网上也是层出不穷,不新鲜,如果你只是想简单学习怎么去创建和访问refs那么你可以看到这里就好了,秉承着我一贯的爱踩坑风格,我决定让自己走一些弯路,再去探索一下更“奇葩”的用法,并且其极有可能在你的业务中用到。

访问DOM的ref对象

上面,我们的Child是一个类组件,其存在实例,于是我们通过三种方式,访问到了这个实例,那么我们思考,如果我们访问的不是一个类组件,而是一个普通DOM节点,会是什么结果呢?我们试一下。

import React, { useRef } from 'react';
const App: React.FC = () => {
  const childRef: any = useRef(null)
  const getRef = () => {
    console.log(childRef.current)
  }
  return (
    
这是一个函数组件
这是一个普通DOM节点我也是
); } export default App

结果是这样的


踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第4张图片
DOMref.png

所以说:

当 ref 属性用于 HTML 元素时,创建的 ref 接收底层 DOM 元素作为其 current 属性。

其效果和document.getElementById一样。但通常,我们很少在react框架中使用document.getElementById这样的语法,那么你就用ref吧!

访问纯函数组件的ref对象(ref转发)

在文章的上面,我们说过,“你只能访问class组件的ref,因为纯函数组件没有实例,但如果你非要获取纯函数组件的ref,你可以使用React.forwardRef”,我们先来试一下,正常访问纯函数组件的Ref会出现什么情况。我们先将Child组件改成纯函数的形式

import React from 'react'
const Child: React.FC = (props: any) => {
    return (
    
这是一个子组件
) } export default Child

我们将Child变成了一个常见的,但是当我们直接去访问其Ref时,就会报这样的错误(强大的ts),当然,如果你使用的是js,也会在执行的过程中报错一些明显的错误。

踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第5张图片
refError.png

上面的意思是,函数组件并不能访问ref,如果我们非要访问怎么办?这个时候就会用到 forwardRef了,如果你在js中使用过 forwardRef,你会知道,使用 forwardRef包装后的纯函数组件第二个参数为 ref就像这样

const Child = (props, ref) => ...
export default React.forwardRef(Child)

但,我们在ts中使用时,我就踩到了第一个坑,ts包出这样的类型错误

踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第6张图片
refserror.png

研究后发现,我们不能将React.FC类型传给 forwardRef,他需要的是一个 ForwardRefRenderFunction类型,查看 ForwardRefRenderFunction类型用法后,我们做出修改如下。

import React from 'react'
const Child: React.ForwardRefRenderFunction = (props: any, ref: any) => {
    return (
    
这是一个子组件
) } export default React.forwardRef(Child)
import React, { useRef } from 'react';
import Child from './components/child'

const App: React.FC = () => {
  const childRef: any = useRef(null)
  const getRef = () => {
    console.log(childRef.current)
  }
  return (
    
这是一个函数组件
); } export default App

这样我们就可以找到正常访问了


踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第7张图片
refResult.png

上面我们做了这样的操作

  • 在父组件中创建了refs对象,并向下传递至Child组件
  • 子组件通过forwardRef的第二个参数接收ref
  • 子组件将接收的ref传至对应的DOM节点或者类组件上,甚至也可以是函数组件上,就重复上面的操作
  • 在父组件中可以访问到子组件的DOM节点或者其某个组件的实例。

因为函数组件并没有实例,所以,我们只能通过访问函数子组件的ref而访问到其下的其他节点或者实例,我们也叫这种操作称为ref转发。ref转发实现了一种将子组件DOM节点暴露给父组件的,提到将DOM节点暴露给父组件,除了ref转发,还有有一种ref回调的形式。下面再介绍一下这种方法。

使用ref回调将DOM节点暴露给父组件

看标题可能比较懵逼,但其实原理很简单,那就是react的父子组件通信。下面用代码来演示一下

import React, { useRef } from 'react';
import Child from './components/child'

const App: React.FC = () => {
  let childRef: any
  const getRef = () => {
    console.log(childRef.current)
  }
  const setRef = (node: any) => {
    childRef = node
  }
  return (
    
这是一个函数组件
); } export default App

我们先传一个回调props给子组件,这时候遇到了一坑


踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第8张图片
refcberror3.png

在js中,这一套操作肯定是行云流水的一套基操,但在ts有了类型约束后,我们不能随便往子组件里面传一些属性了,需要在子组件中定义props的类型。所以,这里插播一条内容

在ts中定义子组件props类型

子组件可能是函数组件也可能是class组件,我们分别来演示一下如果定义其props类型

import React from 'react'
interface childProps {
    childRef?: (node:any) => void
}
const Child: React.ForwardRefRenderFunction = (props: any, ref: any) => {
    console.log(props)
    return (
    
这是一个子组件
) } export default React.forwardRef(Child)

这样我们在父组件中传递props时也不会报错了,也能顺利传递自定义props了。

踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第9张图片
childprops.png

当然了,我们也可以在 React.FC泛型中定义props类型。

import React from 'react'
interface childProps {
    childRef?: (node:any) => void
}
const Child: React.FC = (props: any) => {
    console.log(props)
    return (
    
这是一个子组件
) } export default Child

结果也是一样的,我们还可以在class组件中定义。

import React from 'react'

interface childProps {
    childRef?: T
}

class Child extends React.PureComponent> {

    render () {
        console.log(this.props)
        return 
这是子组件
} } export default Child

或者

import React from 'react'

interface childProps {
    childRef?: (node: any) => void
}

class Child extends React.PureComponent {

    render () {
        console.log(this.props)
        return 
这是子组件
} } export default Child

后者更精确类型。
了解了上面的内容后,我们就可以自由的向子组件中传递props了。然后回到主题上来,接着研究我们的使用ref回调的形式将DOM暴露给父组件。这时就轻车熟路了。我习惯将组件尽量使用精简的纯函数形式,下面来写纯函数

import React from 'react'
interface childProps {
    childRef?: (node:any) => void
}
const Child: React.FC = (props: any) => {
    return (
    
这是一个子组件
) } export default Child

这时候,你父组件中的就可以这样获取子组件暴露的DOM了。

import React from 'react';
import Child from './components/child'

const App: React.FC = () => {
  let childRef: any
  const getRef = () => {
    console.log(childRef) // 在这里获取,注意不是在.current属性中了。因为我们用的是回调。这里容易手滑
  }
  const setRef = (node: any) => {
    childRef = node
  }
  return (
    
这是一个函数组件
); } export default App

结果也是一如既往哦


踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑_第10张图片
refResult.png

写在后面

本文到这里就结束了,大部分代码比较相似,但是也是全部贴了出来,为了做更完成的记录,和尽可能详细的讲解。本文以react+ts为基础,探索react的ref,详细的介绍了Ref的各种使用场景,和在ts类型约束下,可能遇到的坑。

你可能感兴趣的:(踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)