什么是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
屬性上,于是我們的打印結果是這樣的。
結果比較明顯了,這里有一個細節是,我寫在
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
其結果也是一樣的,useRef
同React.createRef()
類似,都會將ref放在其.current
屬性中,不過,useRef
的作用要遠遠大于后者,這里推薦,再次推薦。具體可看官網對其的介紹。
回調函數
React 也支持另一種設置 refs 的方式,稱為“回調 refs”。它能助你更精細地控制何時 refs 被設置和解除。不同于react.createRef
和useRef
返回一個對象,如果想要在 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,最終的實際效果如下
我們在剛開始掛載時執行了方法,這時候我們通過事件獲取其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
結果是這樣的
所以說:
當 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,也會在執行的過程中報錯一些明顯的錯誤。
上面的意思是,函數組件并不能訪問ref,如果我們非要訪問怎么辦?這個時候就會用到
forwardRef
了,如果你在js中使用過forwardRef
,你會知道,使用forwardRef
包裝后的純函數組件第二個參數為 ref
就像這樣
const Child = (props, ref) => ...
export default React.forwardRef(Child)
但,我們在ts中使用時,我就踩到了第一個坑,ts包出這樣的類型錯誤
研究后發現,我們不能將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
這樣我們就可以找到正常訪問了
上面我們做了這樣的操作
- 在父組件中創建了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給子組件,這時候遇到了一坑
在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了。
當然了,我們也可以在
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
結果也是一如既往哦
寫在后面
本文到這里就結束了,大部分代碼比較相似,但是也是全部貼了出來,為了做更完成的記錄,和盡可能詳細的講解。本文以react+ts為基礎,探索react的ref,詳細的介紹了Ref的各種使用場景,和在ts類型約束下,可能遇到的坑。