踩坑日記:以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類型約束下,可能遇到的坑。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。