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

踩坑日记(以ts为基础,详解react|踩坑日记:以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 (这是一个类组件 ); } 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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
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的作用要远远大于后者,这里推荐,再次推荐。具体可看官网对其的介绍。
回调函数 【踩坑日记(以ts为基础,详解react|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)】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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
refError.png
上面的意思是,函数组件并不能访问ref,如果我们非要访问怎么办?这个时候就会用到 forwardRef了,如果你在js中使用过 forwardRef,你会知道,使用 forwardRef包装后的纯函数组件第二个参数为 ref就像这样
const Child = (props, ref) => ... export default React.forwardRef(Child)

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

踩坑日记(以ts为基础,详解react|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
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|踩坑日记:以ts为基础,详解react ref以及其类型约束下的坑)
文章图片
refResult.png 写在后面 本文到这里就结束了,大部分代码比较相似,但是也是全部贴了出来,为了做更完成的记录,和尽可能详细的讲解。本文以react+ts为基础,探索react的ref,详细的介绍了Ref的各种使用场景,和在ts类型约束下,可能遇到的坑。

    推荐阅读