踩坑日記:以ts為基礎(chǔ),詳解react ref以及其類型約束下的坑

react.jpg

什么是ref

Refs 提供了一種方式,允許我們?cè)L問(wèn) DOM 節(jié)點(diǎn)或在 render 方法中創(chuàng)建的 React 元素

上面是官網(wǎng)對(duì)ref的介紹,簡(jiǎn)單概括一下ref的作用為用來(lái)獲取組件的實(shí)例或Dom,并且無(wú)論是你使用Vue框架還是React框架,都不建議過(guò)度使用ref,能用組件通信來(lái)解決的問(wèn)題,一般不推薦使用ref,一般是作為“逃生艙”來(lái)使用,但有一些情況,你不得不使用ref獲取組件的實(shí)例或者DOM,來(lái)打破典型的數(shù)據(jù)流形式組件通信。比如,我們做了某些比較死的表單封裝,想直接通過(guò)父組件調(diào)用其提交方法,比如,你想讓你封裝的“輪播圖”組件直接執(zhí)行其下一步的操作,等等等,程序員可能遇到很多種奇怪的需求,也可能需要你用到ref,這里發(fā)表一下個(gè)人觀點(diǎn),相對(duì)于Vue這種漸進(jìn)式框架而言,React從一開(kāi)始就對(duì)開(kāi)發(fā)者提出的比較嚴(yán)格規(guī)范的要求,所以React的ref并不像Vue中那樣出現(xiàn)的平凡,多數(shù)情況下,還是依照經(jīng)典的數(shù)據(jù)流來(lái)完成一些操作,雖然兩個(gè)兩個(gè)框架都不建議過(guò)度使用ref,比較低的出場(chǎng)率也就注定在使用到它的時(shí)候難免遇到一些坑,今天,我將通過(guò)這篇文章,來(lái)盡可能詳細(xì)的介紹ref的相關(guān)內(nèi)容,并記錄我使用React + ts 訪問(wèn)ref時(shí)遇到的一些坑(本人,react老玩家,ts實(shí)屬新手)。

環(huán)境準(zhǔn)備

  • create-react-app
  • typescript

ref的訪問(wèn)方式

  • React.createRef()
  • useRef(只在函數(shù)組件中使用的hooks)
  • 回調(diào)函數(shù)
  • 字符串(已廢棄,不要再使用了!)

ref 的值根據(jù)節(jié)點(diǎn)的類型而有所不同:

  • 當(dāng)ref屬性用于 HTML 元素時(shí),ref為其底層 DOM 元素。
  • 當(dāng)ref屬性用于自定義 class 組件時(shí),ref 對(duì)象為其接收組件的掛載實(shí)例。
  • 你不能在函數(shù)組件上使用 ref 屬性,因?yàn)樗麄儧](méi)有實(shí)例。如果你想要在函數(shù)組件中使用ref,可以使用forwardRef,但你可以在函數(shù)組件內(nèi)部使用ref屬性,只要他是指向DOM元素或者class組件。

上面介紹了幾個(gè)關(guān)鍵點(diǎn),下面,我們將就上面提到的點(diǎn),做詳細(xì)的介紹和實(shí)例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">
        這是一個(gè)類組件
        <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中訪問(wèn)了類組件Child的ref。我們通過(guò)React.createRef()創(chuàng)建refs,并通過(guò)ref屬性,傳給對(duì)應(yīng)的子組件,因?yàn)樽咏M件是一個(gè)類組件,那么就會(huì)將其實(shí)例,掛載到ref對(duì)象的current屬性上,于是我們的打印結(jié)果是這樣的。

refchild.png

結(jié)果比較明顯了,這里有一個(gè)細(xì)節(jié)是,我寫(xiě)在constructor中的打印結(jié)果為null,而寫(xiě)在componentDidMount生命周期里才能正常打印,所以,這里有一點(diǎn)需要主要的是ref是組件或者DOM掛載后才可以訪問(wèn)到的,這一點(diǎn)需要注意,你在訪問(wèn)組件ref時(shí),必須確保其已經(jīng)掛載
我們上面的代碼中,APP也是一個(gè)類組件,那么如果APP是函數(shù)組件呢?我們也是可以正常使用React.createRef(),只不過(guò)純函數(shù)組件沒(méi)有生命周期,我們可以通過(guò)事件來(lái)訪問(wèn)(這時(shí)候組件一定是掛載完成的),通常將 Refs 分配給實(shí)例屬性,以便可以在整個(gè)組件中引用它們。

const App: React.FC = () => {
  const childRef: any = React.createRef()
  const getRef = () => {
    console.log(childRef.current)
  }
  return (
    <div className="App">
      這是一個(gè)函數(shù)組件
      <Child ref={childRef}/>
      <button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
    </div>
  );
}
export default App

我們也是可以正常獲取結(jié)果的。其實(shí)對(duì)于純函數(shù)組件,我更推薦使用useRef這個(gè)hooks來(lái)完成,因?yàn)?a target="_blank">useRef的優(yōu)勢(shì)還是比傳統(tǒng)的獲取ref的形式要多很多,因?yàn)閡seRef不僅僅可以存ref,還可以存任何值,你可以用它來(lái)存變量等,當(dāng)然,這是這個(gè)hooks本身的優(yōu)勢(shì),今天我們主要說(shuō)ref,還是說(shuō)說(shuō)怎么使用useRef來(lái)訪問(wèn)ref吧

useRef

react推出hooks可謂是讓react變得更受歡迎了,也更加的舒服了,其中的refRef就可以幫助我們?cè)诩兒瘮?shù)組件中訪問(wèn)refs對(duì)象,于是,對(duì)于上面,我們使用react.createRef()在純函數(shù)中訪問(wèn)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">
      這是一個(gè)函數(shù)組件
      <Child ref={childRef}/>
      <button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
    </div>
  );
}
export default App

其結(jié)果也是一樣的,useRefReact.createRef()類似,都會(huì)將ref放在其.current屬性中,不過(guò),useRef的作用要遠(yuǎn)遠(yuǎn)大于后者,這里推薦,再次推薦。具體可看官網(wǎng)對(duì)其的介紹。

回調(diào)函數(shù)

React 也支持另一種設(shè)置 refs 的方式,稱為“回調(diào) refs”。它能助你更精細(xì)地控制何時(shí) refs 被設(shè)置和解除。不同于react.createRefuseRef返回一個(gè)對(duì)象,如果想要在 React 綁定或解綁 DOM 節(jié)點(diǎn)的 ref 時(shí)運(yùn)行某些代碼,則需要使用回調(diào)ref來(lái)實(shí)現(xiàn)。
我們還是使用上面的代碼(實(shí)際開(kāi)發(fā)中,我已經(jīng)很少寫(xiě)類組件了~),換成回調(diào)函數(shù)的形式。

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">
      這是一個(gè)函數(shù)組件
      {isMount && <Child ref={setRef}/>}
      <button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
      <button onClick={unMountChild}>點(diǎn)擊卸載子組件</button>
    </div>
  );
}
export default App

在上面的代碼中,我們通過(guò)傳入回調(diào)的方式,將refs對(duì)象賦給了childRef變量,并可以在某一事件中獲取它,這一點(diǎn),和其他兩種方式?jīng)]有差別,差別在于,我們可以在Child組件掛載或者卸載的時(shí)候執(zhí)行一些方法,我們通過(guò)一個(gè)狀態(tài)控制了子組件的掛載狀態(tài),來(lái)做這個(gè)demo,最終的實(shí)際效果如下


refChild2.png

我們?cè)趧傞_(kāi)始掛載時(shí)執(zhí)行了方法,這時(shí)候我們通過(guò)事件獲取其refs對(duì)象,當(dāng)修改狀態(tài)使組件卸載,可以看到再次執(zhí)行了方法,并且這時(shí)候也獲取不到refs對(duì)象了。這就是refs回調(diào)的作用。

小結(jié):上面我們介紹了幾種訪問(wèn)refs對(duì)象和創(chuàng)建refs對(duì)象的方法,這樣的文章在網(wǎng)上也是層出不窮,不新鮮,如果你只是想簡(jiǎn)單學(xué)習(xí)怎么去創(chuàng)建和訪問(wèn)refs那么你可以看到這里就好了,秉承著我一貫的愛(ài)踩坑風(fēng)格,我決定讓自己走一些彎路,再去探索一下更“奇葩”的用法,并且其極有可能在你的業(yè)務(wù)中用到。

訪問(wèn)DOM的ref對(duì)象

上面,我們的Child是一個(gè)類組件,其存在實(shí)例,于是我們通過(guò)三種方式,訪問(wèn)到了這個(gè)實(shí)例,那么我們思考,如果我們?cè)L問(wèn)的不是一個(gè)類組件,而是一個(gè)普通DOM節(jié)點(diǎn),會(huì)是什么結(jié)果呢?我們?cè)囈幌隆?/p>

import React, { useRef } from 'react';
const App: React.FC = () => {
  const childRef: any = useRef(null)
  const getRef = () => {
    console.log(childRef.current)
  }
  return (
    <div className="App">
      這是一個(gè)函數(shù)組件
      <div ref={childRef}>這是一個(gè)普通DOM節(jié)點(diǎn)<span>我也是</span></div>
      <button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
    </div>
  );
}
export default App

結(jié)果是這樣的


DOMref.png

所以說(shuō):

當(dāng) ref 屬性用于 HTML 元素時(shí),創(chuàng)建的 ref 接收底層 DOM 元素作為其 current 屬性。

其效果和document.getElementById一樣。但通常,我們很少在react框架中使用document.getElementById這樣的語(yǔ)法,那么你就用ref吧!

訪問(wèn)純函數(shù)組件的ref對(duì)象(ref轉(zhuǎn)發(fā))

在文章的上面,我們說(shuō)過(guò),“你只能訪問(wèn)class組件的ref,因?yàn)榧兒瘮?shù)組件沒(méi)有實(shí)例,但如果你非要獲取純函數(shù)組件的ref,你可以使用React.forwardRef”,我們先來(lái)試一下,正常訪問(wèn)純函數(shù)組件的Ref會(huì)出現(xiàn)什么情況。我們先將Child組件改成純函數(shù)的形式

import React from 'react'
const Child: React.FC = (props: any) => {
    return (
    <div>
        這是一個(gè)子組件
    </div>
    )
}
export default Child

我們將Child變成了一個(gè)常見(jiàn)的,但是當(dāng)我們直接去訪問(wèn)其Ref時(shí),就會(huì)報(bào)這樣的錯(cuò)誤(強(qiáng)大的ts),當(dāng)然,如果你使用的是js,也會(huì)在執(zhí)行的過(guò)程中報(bào)錯(cuò)一些明顯的錯(cuò)誤。

refError.png

上面的意思是,函數(shù)組件并不能訪問(wèn)ref,如果我們非要訪問(wèn)怎么辦?這個(gè)時(shí)候就會(huì)用到forwardRef了,如果你在js中使用過(guò)forwardRef,你會(huì)知道,使用forwardRef包裝后的純函數(shù)組件第二個(gè)參數(shù)為 ref就像這樣

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

但,我們?cè)趖s中使用時(shí),我就踩到了第一個(gè)坑,ts包出這樣的類型錯(cuò)誤

refserror.png

研究后發(fā)現(xiàn),我們不能將React.FC類型傳給forwardRef,他需要的是一個(gè)ForwardRefRenderFunction類型,查看ForwardRefRenderFunction類型用法后,我們做出修改如下。

import React from 'react'
const Child: React.ForwardRefRenderFunction<unknown, {}> = (props: any, ref: any) => {
    return (
    <div ref={ref}>
        這是一個(gè)子組件
    </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">
      這是一個(gè)函數(shù)組件
      <Child ref={childRef}/>
      <button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
    </div>
  );
}
export default App

這樣我們就可以找到正常訪問(wèn)了


refResult.png

上面我們做了這樣的操作

  • 在父組件中創(chuàng)建了refs對(duì)象,并向下傳遞至Child組件
  • 子組件通過(guò)forwardRef的第二個(gè)參數(shù)接收ref
  • 子組件將接收的ref傳至對(duì)應(yīng)的DOM節(jié)點(diǎn)或者類組件上,甚至也可以是函數(shù)組件上,就重復(fù)上面的操作
  • 在父組件中可以訪問(wèn)到子組件的DOM節(jié)點(diǎn)或者其某個(gè)組件的實(shí)例。

因?yàn)楹瘮?shù)組件并沒(méi)有實(shí)例,所以,我們只能通過(guò)訪問(wèn)函數(shù)子組件的ref而訪問(wèn)到其下的其他節(jié)點(diǎn)或者實(shí)例,我們也叫這種操作稱為ref轉(zhuǎn)發(fā)。ref轉(zhuǎn)發(fā)實(shí)現(xiàn)了一種將子組件DOM節(jié)點(diǎn)暴露給父組件的,提到將DOM節(jié)點(diǎn)暴露給父組件,除了ref轉(zhuǎn)發(fā),還有有一種ref回調(diào)的形式。下面再介紹一下這種方法。

使用ref回調(diào)將DOM節(jié)點(diǎn)暴露給父組件

看標(biāo)題可能比較懵逼,但其實(shí)原理很簡(jiǎn)單,那就是react的父子組件通信。下面用代碼來(lái)演示一下

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">
      這是一個(gè)函數(shù)組件
      <Child childRef={setRef}/>
      <button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
    </div>
  );
}
export default App

我們先傳一個(gè)回調(diào)props給子組件,這時(shí)候遇到了一坑


refcberror3.png

在js中,這一套操作肯定是行云流水的一套基操,但在ts有了類型約束后,我們不能隨便往子組件里面?zhèn)饕恍傩粤耍枰谧咏M件中定義props的類型。所以,這里插播一條內(nèi)容

在ts中定義子組件props類型

子組件可能是函數(shù)組件也可能是class組件,我們分別來(lái)演示一下如果定義其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}>
        這是一個(gè)子組件
    </div>
    )
}
export default React.forwardRef(Child)

這樣我們?cè)诟附M件中傳遞props時(shí)也不會(huì)報(bào)錯(cuò)了,也能順利傳遞自定義props了。

childprops.png

當(dāng)然了,我們也可以在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>
        這是一個(gè)子組件
    </div>
    )
}
export default Child

結(jié)果也是一樣的,我們還可以在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

后者更精確類型。
了解了上面的內(nèi)容后,我們就可以自由的向子組件中傳遞props了。然后回到主題上來(lái),接著研究我們的使用ref回調(diào)的形式將DOM暴露給父組件。這時(shí)就輕車熟路了。我習(xí)慣將組件盡量使用精簡(jiǎn)的純函數(shù)形式,下面來(lái)寫(xiě)純函數(shù)

import React from 'react'
interface childProps {
    childRef?: (node:any) => void
}
const Child: React.FC<childProps> = (props: any) => {
    return (
    <div ref={props.childRef}>
        這是一個(gè)子組件
    </div>
    )
}
export default Child

這時(shí)候,你父組件中的就可以這樣獲取子組件暴露的DOM了。

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

const App: React.FC = () => {
  let childRef: any
  const getRef = () => {
    console.log(childRef) // 在這里獲取,注意不是在.current屬性中了。因?yàn)槲覀冇玫氖腔卣{(diào)。這里容易手滑
  }
  const setRef = (node: any) => {
    childRef = node
  }
  return (
    <div className="App">
      這是一個(gè)函數(shù)組件
      <Child childRef={setRef}/>
      <button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
    </div>
  );
}
export default App

結(jié)果也是一如既往哦


refResult.png

寫(xiě)在后面

本文到這里就結(jié)束了,大部分代碼比較相似,但是也是全部貼了出來(lái),為了做更完成的記錄,和盡可能詳細(xì)的講解。本文以react+ts為基礎(chǔ),探索react的ref,詳細(xì)的介紹了Ref的各種使用場(chǎng)景,和在ts類型約束下,可能遇到的坑。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374