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

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 (
      <div className="App">
        这是一个类组件
        <Child ref={this.childRef}/>
      </div>
    );
  }
  componentDidMount () {
    console.log(this.childRef.current)
  }
}
export default App;

child.tsx

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

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

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 (
    <div className="App">
      这是一个函数组件
      <Child ref={childRef}/>
      <button onClick={getRef}>点击获取组件ref</button>
    </div>
  );
}
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 (
    <div className="App">
      这是一个函数组件
      <Child ref={childRef}/>
      <button onClick={getRef}>点击获取组件ref</button>
    </div>
  );
}
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 (
    <div className="App">
      这是一个函数组件
      {isMount && <Child ref={setRef}/>}
      <button onClick={getRef}>点击获取组件ref</button>
      <button onClick={unMountChild}>点击卸载子组件</button>
    </div>
  );
}
export default App

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


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 (
    <div className="App">
      这是一个函数组件
      <div ref={childRef}>这是一个普通DOM节点<span>我也是</span></div>
      <button onClick={getRef}>点击获取组件ref</button>
    </div>
  );
}
export default App

结果是这样的


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 (
    <div>
        这是一个子组件
    </div>
    )
}
export default Child

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

refError.png

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

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

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

refserror.png

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

import React from 'react'
const Child: React.ForwardRefRenderFunction<unknown, {}> = (props: any, ref: any) => {
    return (
    <div ref={ref}>
        这是一个子组件
    </div>
    )
}
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 (
    <div className="App">
      这是一个函数组件
      <Child ref={childRef}/>
      <button onClick={getRef}>点击获取组件ref</button>
    </div>
  );
}
export default App

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


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 (
    <div className="App">
      这是一个函数组件
      <Child childRef={setRef}/>
      <button onClick={getRef}>点击获取组件ref</button>
    </div>
  );
}
export default App

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


refcberror3.png

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

在ts中定义子组件props类型

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

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

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

childprops.png

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

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

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

import React from 'react'

interface childProps<T> {
    childRef?: T
}

class Child<T> extends React.PureComponent<childProps<T>> {

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

或者

import React from 'react'

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

class Child extends React.PureComponent<childProps> {

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

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

import React from 'react'
interface childProps {
    childRef?: (node:any) => void
}
const Child: React.FC<childProps> = (props: any) => {
    return (
    <div ref={props.childRef}>
        这是一个子组件
    </div>
    )
}
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 (
    <div className="App">
      这是一个函数组件
      <Child childRef={setRef}/>
      <button onClick={getRef}>点击获取组件ref</button>
    </div>
  );
}
export default App

结果也是一如既往哦


refResult.png

写在后面

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352